/*
 *  $Id: alicona.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2011-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-FREEDESKTOP]
 * <mime-type type="application/x-alicona-imaging-al3d">
 *   <comment></comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="AliconaImaging\x00\x0d\x0a"/>
 *   </magic>
 *   <glob pattern="*.al3d"/>
 *   <glob pattern="*.AL3D"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Alicona Imaging surfaces.
 * 0 string AliconaImaging\x00\x0d\x0a Alicona Imaging surface data
 * >17 string Version\0\0\0\0\0\0\0\0\0\0\0\0\0
 * >>37 string >\0 version %s
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Alicona Imaging AL3D data
 * .al3d
 * Read
 **/

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

#include "err.h"

#define MAGIC "AliconaImaging\x00\r\n"
#define MAGIC_SIZE (sizeof(MAGIC)-1)

#define EXTENSION ".al3d"

enum {
    KEY_SIZE = 20,
    VALUE_SIZE = 30,
    CRLF_SIZE = 2,
    TAG_SIZE = KEY_SIZE + VALUE_SIZE + CRLF_SIZE,
    COMMENT_SIZE = 256,
    MIN_HEADER_SIZE = MAGIC_SIZE + 2*TAG_SIZE + COMMENT_SIZE,
    ICON_SIZE = 68400,
};

typedef struct {
    gchar key[KEY_SIZE];
    gchar value[VALUE_SIZE];
    gchar crlf[CRLF_SIZE];
} Al3DTag;

typedef struct {
    const Al3DTag *version;
    const Al3DTag *counter;
    const Al3DTag *tags;
    const gchar *comment;
    const guchar *icon_data;
    const guchar *depth_data;
    const guchar *texture_data;
    /* Cached parsed values. */
    guint ntags;
    guint xres;
    guint yres;
    guint nplanes;
    gdouble dx;
    gdouble dy;
    guint iconoffset;
    guint textureoffset;
    guint depthoffset;
} Al3DFile;

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           set_title         (GwyFile *file,
                                         guint id,
                                         const gchar *name,
                                         gint component);
static void           add_meta          (GwyFile *file,
                                         guint id,
                                         const Al3DFile *afile);
static gchar*         texture_lo_ptr    (const Al3DTag *tag);
static gchar*         texture_ptr       (const Al3DTag *tag);
static GwyField*      read_depth_image  (const Al3DFile *afile,
                                         const guchar *buffer,
                                         GwyNield **badmask);
static GwyField*      read_hi_lo_texture(const Al3DFile *afile,
                                         const Al3DTag *hitag,
                                         const Al3DTag *lotag,
                                         const guchar *buffer,
                                         GError **error);
static GwyField*      read_texture      (const Al3DFile *afile,
                                         const Al3DTag *tag,
                                         guint planeno,
                                         const guchar *buffer,
                                         GError **error);
static gboolean       file_load_header  (Al3DFile *afile,
                                         const guchar *buffer,
                                         gsize size,
                                         GError **error);
static const Al3DTag* find_tag          (const Al3DFile *afile,
                                         const gchar *name,
                                         GError **error);
static gboolean       read_uint_tag     (const Al3DFile *afile,
                                         const gchar *name,
                                         guint *retval,
                                         GError **error);
static gboolean       read_float_tag    (const Al3DFile *afile,
                                         const gchar *name,
                                         gdouble *retval,
                                         GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Alicona Imaging AL3D files."),
    "Yeti <yeti@gwyddion.net>",
    "0.6",
    "David Nečas (Yeti)",
    "2011",
};

GWY_MODULE_QUERY2(module_info, alicona)

static gboolean
module_register(void)
{
    gwy_file_func_register("alicona",
                           N_("Alicona Imaging AL3D files (.al3d)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

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

    if (fileinfo->buffer_len > MAGIC_SIZE && memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) == 0)
        return 100;

    return 0;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyField *field = NULL;
    Al3DFile afile;
    const guchar *buffer = NULL;
    guint firstfreepos;
    gsize size;
    GError *err = NULL;
    guint i, id = 0;

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

    if (!file_load_header(&afile, buffer, size, error))
        goto fail;

    if (!read_uint_tag(&afile, "Cols", &afile.xres, error) || !read_uint_tag(&afile, "Rows", &afile.yres, error))
        goto fail;

    if (err_DIMENSION(error, afile.xres) || err_DIMENSION(error, afile.yres))
        goto fail;

    if (!read_float_tag(&afile, "PixelSizeXMeter", &afile.dx, error)
        || !read_float_tag(&afile, "PixelSizeYMeter", &afile.dy, error))
        goto fail;

    sanitise_real_size(&afile.dx, "x step");
    sanitise_real_size(&afile.dy, "y step");
    firstfreepos = MAGIC_SIZE + (TAG_SIZE + 2)*afile.ntags + COMMENT_SIZE;
    read_uint_tag(&afile, "IconOffset", &afile.iconoffset, NULL);
    if (afile.iconoffset) {
        if (afile.iconoffset < firstfreepos
            || size < firstfreepos + ICON_SIZE
            || afile.iconoffset > size - ICON_SIZE) {
            err_INVALID(error, "IconOffset");
            goto fail;
        }
        firstfreepos += ICON_SIZE;
    }

    read_uint_tag(&afile, "DepthImageOffset", &afile.depthoffset, NULL);
    if (afile.depthoffset) {
        guint rowstride = (afile.xres*sizeof(gfloat) + 7)/8*8;
        guint imagesize = afile.yres*rowstride;
        if (afile.depthoffset < firstfreepos
            || size < firstfreepos + imagesize
            || afile.depthoffset > size - imagesize) {
            err_INVALID(error, "DepthImageOffset");
            goto fail;
        }
        firstfreepos += imagesize;
    }

    /* XXX: Some files apparently do not give NumberOfPlanes but they have 3 planes.  Try to guess the number from
     * TexturePtr? */
    read_uint_tag(&afile, "NumberOfPlanes", &afile.nplanes, NULL);
    read_uint_tag(&afile, "TextureImageOffset", &afile.textureoffset, NULL);
    if (afile.nplanes && afile.textureoffset) {
        guint rowstride = (afile.xres + 7)/8*8;
        guint planesize = afile.yres*rowstride;
        if (afile.textureoffset < firstfreepos
            || size < firstfreepos + planesize*afile.nplanes
            || afile.textureoffset > size - planesize*afile.nplanes) {
            err_INVALID(error, "TextureImageOffset");
            goto fail;
        }
        firstfreepos += planesize*afile.nplanes;
    }

    if (firstfreepos == MAGIC_SIZE + (TAG_SIZE + 2)*afile.ntags + COMMENT_SIZE) {
        err_NO_DATA(error);
        goto fail;
    }

    file = gwy_file_new_in_construction();

    if (afile.depthoffset) {
        GwyNield *badmask = NULL;

        field = read_depth_image(&afile, buffer, &badmask);
        gwy_file_set_image(file, id, field);
        if ((badmask = gwy_remove_bad_image_data(field, badmask)))
            gwy_file_pass_image_mask(file, id, badmask);
        g_object_unref(field);
        set_title(file, id, "Depth", -1);
        add_meta(file, id, &afile);
        gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, filename);
        id++;
    }

    for (i = 0; i < afile.ntags; i++) {
        const Al3DTag *tag = afile.tags + i;
        gchar *name;

        if ((name = texture_lo_ptr(tag))) {
            gchar *hikey = gwy_strreplace(tag->key, "LoPtr", "HiPtr", 1);
            const Al3DTag *hitag = find_tag(&afile, hikey, NULL);
            g_free(hikey);

            gwy_debug("loptr tag <%s> (%s) = %s", tag->key, name, tag->value);
            field = read_hi_lo_texture(&afile, hitag, tag, buffer, &err);
            if (!field) {
                g_warning("%s", err->message);
                g_clear_error(&err);
                g_free(name);
                continue;
            }
            gwy_file_pass_image(file, id, field);
            set_title(file, id, name, -1);
            add_meta(file, id, &afile);
            g_free(name);
            gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, filename);
            id++;
        }
        else if ((name = texture_ptr(tag))) {
            gchar **planes = g_strsplit(tag->value, ";", 0);
            guint nplanes = g_strv_length(planes);
            guint j, planeno;

            gwy_debug("ptr tag <%s> (%s) = %s", tag->key, name, tag->value);
            for (j = 0; planes[j]; j++) {
                planeno = atoi(planes[j]);
                field = read_texture(&afile, tag, planeno, buffer, &err);
                if (!field) {
                    g_warning("%s", err->message);
                    g_clear_error(&err);
                    continue;
                }
                gwy_file_pass_image(file, id, field);
                set_title(file, id, name, nplanes > 1 ? j : -1);
                add_meta(file, id, &afile);
                gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, filename);
                id++;
            }
            g_strfreev(planes);
            g_free(name);
        }
    }

fail:
    gwy_unmap_file_contents(buffer);
    if (file && !gwy_dict_get_n_items(GWY_DICT(file))) {
        g_object_unref(file);
        file = NULL;
    }

    return file;
}

static void
set_title(GwyFile *file,
          guint id,
          const gchar *name,
          gint component)
{
    const gchar *gradient = NULL;
    gchar *title;

    if (component == -1)
        title = g_strdup(name);
    else if (component == 0) {
        title = g_strdup_printf("%s (R)", name);
        gradient = "RGB-Red";
    }
    else if (component == 1) {
        title = g_strdup_printf("%s (G)", name);
        gradient = "RGB-Green";
    }
    else if (component == 2) {
        title = g_strdup_printf("%s (B)", name);
        gradient = "RGB-Blue";
    }
    else
        title = g_strdup_printf("%s (%u)", name, component);

    gwy_file_pass_title(file, GWY_FILE_IMAGE, id, title);
    if (gradient)
        gwy_file_set_palette(file, GWY_FILE_IMAGE, id, gradient);
}

static void
add_meta(GwyFile *file,
         guint id,
         const Al3DFile *afile)
{
    GwyDict *meta = gwy_dict_new();
    guint i;

    gwy_dict_set_string_by_name(meta, afile->version->key, afile->version->value);
    for (i = 0; i < afile->ntags; i++) {
        const Al3DTag *tag = afile->tags + i;

        if (gwy_stramong(tag->key,
                         "DirSpacer", "PlaceHolder", "Cols", "Rows", "NumberOfPlanes", "ImageCode",
                         "PixelSizeXMeter", "PixelSizeYMeter", "InvalidPixelValue",
                         NULL)
            || strstr(tag->key, "Ptr")
            || g_str_has_suffix(tag->key, "Offset"))
            continue;

        gwy_dict_set_string_by_name(meta, tag->key, tag->value);
    }

    if (*(afile->comment)) {
        gchar *p = gwy_convert_to_utf8(afile->comment, -1, "ISO-8859-1");
        if (p)
            gwy_dict_pass_string_by_name(meta, "Comment", p);
    }

    gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, meta);
}

static gchar*
texture_lo_ptr(const Al3DTag *tag)
{
    const gchar *p = strstr(tag->key, "LoPtr");

    if (!p || p == tag->key)
        return NULL;

    return gwy_strreplace(tag->key, "LoPtr", "", 1);
}

static gchar*
texture_ptr(const Al3DTag *tag)
{
    const gchar *p = strstr(tag->key, "Ptr");

    if (!p || p == tag->key)
        return NULL;

    if (strstr(tag->key, "LoPtr") || strstr(tag->key, "HiPtr"))
        return NULL;

    return gwy_strreplace(tag->key, "Ptr", "", 1);
}

static GwyField*
read_depth_image(const Al3DFile *afile,
                 const guchar *buffer,
                 GwyNield **badmask)
{
    GwyField *field;
    GwyNield *mask = NULL;
    guint rowstride = (afile->xres*sizeof(gfloat) + 7)/8*8;
    gdouble *d;
    gint *m = NULL;
    guint i;
    gdouble invalid_value = NAN, invalid_eps;
    gboolean invalid_is_nan;
    guint xres = afile->xres, yres = afile->yres;

    read_float_tag(afile, "InvalidPixelValue", &invalid_value, NULL);
    invalid_is_nan = gwy_isnan(invalid_value);
    /* If InvalidPixelValue is finite it does not have full double precision. After all, the raw data are single
     * precision.  Use a loose comparison to catch invalid pixels. */
    if (!invalid_is_nan)
        invalid_eps = 1.5e-7 * invalid_value;

    field = gwy_field_new(xres, yres, afile->dx*xres, afile->dy*yres, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(field), "m");

    d = gwy_field_get_data(field);
    for (i = 0; i < yres; i++) {
        gwy_convert_raw_data(buffer + afile->depthoffset + i*rowstride, xres, 1,
                             GWY_RAW_DATA_FLOAT, GWY_BYTE_ORDER_LITTLE_ENDIAN, d + i*xres, 1.0, 0.0);
    }
    for (i = 0; i < xres*yres; i++) {
        if (gwy_isnan(d[i]) || (!invalid_is_nan && fabs(d[i] - invalid_value) < invalid_eps)) {
            if (!mask) {
                mask = gwy_field_new_nield_alike(field);
                m = gwy_nield_get_data(mask);
            }
            d[i] = 0.0;
            m[i] = 1;
        }
    }

    *badmask = mask;

    return field;
}

static gboolean
check_plane_no(const Al3DFile *afile,
               const gchar *key,
               guint planeno,
               GError **error)
{
    if (planeno < afile->nplanes)
        return TRUE;

    g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                _("Invalid plane number %u in tag ‘%s’."), planeno, key);
    return FALSE;
}

static GwyField*
read_hi_lo_texture(const Al3DFile *afile,
                   const Al3DTag *hitag,
                   const Al3DTag *lotag,
                   const guchar *buffer,
                   GError **error)
{
    GwyField *field;
    guint xres = afile->xres, yres = afile->yres;
    guint rowstride = (afile->xres + 7)/8*8;
    guint hiplaneno = (guint)atoi(hitag->value);
    guint loplaneno = (guint)atoi(lotag->value);
    guint planesize = yres*rowstride;
    gdouble *d;
    guint i, j;

    if (!check_plane_no(afile, hitag->key, hiplaneno, error)
        || !check_plane_no(afile, lotag->key, loplaneno, error))
        return NULL;

    field = gwy_field_new(xres, yres, afile->dx*xres, afile->dy*yres, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");

    d = gwy_field_get_data(field);
    for (i = 0; i < yres; i++) {
        const guchar *phi = (buffer + afile->textureoffset + hiplaneno*planesize + i*rowstride);
        const guchar *plo = (buffer + afile->textureoffset + loplaneno*planesize + i*rowstride);

        for (j = 0; j < xres; j++, d++, plo++, phi++)
            *d = (*plo | ((guint)*phi << 8))/65536.0;
    }

    return field;
}

static GwyField*
read_texture(const Al3DFile *afile,
             const Al3DTag *tag,
             guint planeno,
             const guchar *buffer,
             GError **error)
{
    GwyField *field;
    guint xres = afile->xres, yres = afile->yres;
    guint rowstride = (afile->xres + 7)/8*8;
    guint planesize = yres*rowstride;
    gdouble *d;
    guint i, j;

    if (!check_plane_no(afile, tag->key, planeno, error))
        return NULL;

    field = gwy_field_new(xres, yres, afile->dx*xres, afile->dy*yres, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");

    d = gwy_field_get_data(field);
    for (i = 0; i < yres; i++) {
        const guchar *p = (buffer + afile->textureoffset + planeno*planesize + i*rowstride);

        for (j = 0; j < xres; j++, d++, p++)
            *d = *p/256.0;
    }

    return field;
}

static gboolean
check_tag(const Al3DTag *tag,
          GError **error)
{
    guint i;

    gwy_debug("tag <%.20s>", tag->key);
    if (tag->key[KEY_SIZE-1]) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Header tag key is not nul-terminated."));
        return FALSE;
    }
    if (!tag->key[0]) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Header tag key is empty."));
        return FALSE;
    }
    for (i = strlen(tag->key); i < KEY_SIZE-1; i++) {
        if (tag->key[i]) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Header tag ‘%s’ key is not nul-padded."),
                        tag->key);
            return FALSE;
        }
    }
    if (tag->crlf[0] != '\r' || tag->crlf[1] != '\n') {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Header tag ‘%s’ lacks CRLF terminator."),
                    tag->key);
        return FALSE;
    }
    if (tag->value[VALUE_SIZE-1]) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Header tag ‘%s’ value is not nul-terminated."),
                    tag->key);
        return FALSE;
    }
    if (gwy_stramong(tag->key, "DirSpacer", "PlaceHolder", NULL))
        return TRUE;
    for (i = strlen(tag->value); i < VALUE_SIZE-1; i++) {
        if (tag->value[i]) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Header tag ‘%s’ value is not nul-padded."),
                        tag->key);
            return FALSE;
        }
    }
    return TRUE;
}

static gboolean
file_load_header(Al3DFile *afile,
                 const guchar *buffer,
                 gsize size,
                 GError **error)
{
    const guchar *p = buffer;
    gsize expected_size;
    guint i;

    gwy_clear(afile, 1);
    if (size < MIN_HEADER_SIZE) {
        err_TOO_SHORT(error);
        return FALSE;
    }
    if (memcmp(p, MAGIC, MAGIC_SIZE) != 0) {
        err_FILE_TYPE(error, "Al3D");
        return FALSE;
    }
    p += MAGIC_SIZE;

    afile->version = (const Al3DTag*)p;
    p += TAG_SIZE;
    if (!check_tag(afile->version, error))
        return FALSE;
    if (!gwy_strequal(afile->version->key, "Version")) {
        err_MISSING_FIELD(error, "Version");
        return FALSE;
    }

    afile->counter = (const Al3DTag*)p;
    p += TAG_SIZE;
    if (!check_tag(afile->counter, error))
        return FALSE;
    if (!gwy_strequal(afile->counter->key, "TagCount")) {
        err_MISSING_FIELD(error, "TagCount");
        return FALSE;
    }

    afile->ntags = (guint)atoi(afile->counter->value);
    expected_size = TAG_SIZE*afile->ntags;
    if ((gsize)(size - (p - buffer)) < expected_size + COMMENT_SIZE) {
        err_TRUNCATED_HEADER(error);
        return FALSE;
    }

    afile->tags = (const Al3DTag*)p;
    p += afile->ntags*TAG_SIZE;
    for (i = 0; i < afile->ntags; i++) {
        if (!check_tag(afile->tags + i, error))
            return FALSE;
    }

    afile->comment = p;
    if (afile->comment[COMMENT_SIZE-1] != '\n'
        || afile->comment[COMMENT_SIZE-2] != '\r') {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Comment lacks CRLF termination."));
        return FALSE;
    }
    if (afile->comment[COMMENT_SIZE-3]) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Comment is not nul-terminated."));
        return FALSE;
    }

    return TRUE;
}

static const Al3DTag*
find_tag(const Al3DFile *afile,
         const gchar *name,
         GError **error)
{
    guint i;

    if (gwy_strequal(name, "Version"))
        return afile->version;
    if (gwy_strequal(name, "TagCount"))
        return afile->counter;

    for (i = 0; i < afile->ntags; i++) {
        if (gwy_strequal(afile->tags[i].key, name))
            return afile->tags + i;
    }

    err_MISSING_FIELD(error, name);
    return NULL;
}

static gboolean
read_uint_tag(const Al3DFile *afile,
              const gchar *name,
              guint *retval,
              GError **error)
{
    const Al3DTag *tag;

    if (!(tag = find_tag(afile, name, error)))
        return FALSE;

    *retval = (guint)atol(tag->value);
    return TRUE;
}

static gboolean
read_float_tag(const Al3DFile *afile,
               const gchar *name,
               gdouble *retval,
               GError **error)
{
    const Al3DTag *tag;

    if (!(tag = find_tag(afile, name, error)))
        return FALSE;

    *retval = g_ascii_strtod(tag->value, NULL);
    return TRUE;
}

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