/*
 *  $Id: asciicmap.c 29571 2026-03-05 15:49:32Z yeti-dn $
 *  Copyright (C) 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-USERGUIDE]
 * Curve map exported to text
 * .txt
 * Read Export
 **/

#include "config.h"
#include <string.h>
#include <stdlib.h>

#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>
#include <gwy.h>

#include "err.h"

#define MAGIC "# Curve map text export"
#define MAGIC_SIZE (sizeof(MAGIC)-1)

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 GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Reads curve maps exported to text."),
    "Yeti <yeti@gwyddion.net>",
    "2.0",
    "David Nečas (Yeti)",
    "2025",
};

GWY_MODULE_QUERY2(module_info, asciicmap)

static gboolean
module_register(void)
{
    gwy_file_func_register("asciicmap",
                           N_("Curve map text export (.txt)"),
                           &detect_file, &load_file, NULL, NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    enum { NHEADERS = 9 };
    static const gchar *expected_headers[NHEADERS] = {
        "Channel", "Rows", "Columns", "Curves", "Segment", "Width", "Height", "Curve1", "CurveUnit1",
    };
    static const guint header_lengths[NHEADERS] = { 7, 4, 7, 6, 7, 5, 6, 6, 10 };
    const guchar *hash;
    guint i, tosearch, ngood = 0;
    gint score = 0;

    g_return_val_if_fail(!only_name, 0);

    if (fileinfo->buffer_len < 60)
        return 0;

    if (memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) != 0
        || (fileinfo->head[MAGIC_SIZE] != '\n' && fileinfo->head[MAGIC_SIZE] != '\r'))
        return 0;
    score = 75;

    hash = fileinfo->head;
    tosearch = MIN(fileinfo->buffer_len - MAGIC_SIZE, 1024);
    while (*hash && (hash = memchr(hash+1, '#', tosearch - (hash+1 - fileinfo->head)))) {
        hash++;
        while (g_ascii_isspace(hash[0]))
            hash++;
        for (i = 0; i < NHEADERS; i++) {
            guint len = header_lengths[i];
            if (strncmp(hash, expected_headers[i], len))
                continue;
            if (hash[len] != ':')
                continue;
            ngood++;
            break;
        }
    }

    for (i = 0; i < ngood; i++)
        score = score + (100 - score)/2;

    return score;
}

static gsize
read_a_few_lines(const gchar *buffer, gsize len, GString *str, guint n)
{
    gsize i = 0;
    guint lineno;

    g_string_truncate(str, 0);
    for (lineno = 0; lineno < n; lineno++) {
        while (i < len && buffer[i] != '\n' && buffer[i] != '\r')
            i++;
        /* Hitting the end means failure unless we are already reading the last requested line. */
        if (i == len) {
            if (lineno+1 == n) {
                g_string_append_len(str, buffer, len);
                return len;
            }
            return 0;
        }
        /* Skip exactly one CR, one LF or one CRLF. Empty lines matter. */
        if (buffer[i] == '\r' && buffer[i+1] == '\r')
            i++;
        i++;
    }
    g_string_append_len(str, buffer, i);
    return i;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    const guchar *buffer, *p;
    GString *str = NULL;
    GError *err = NULL;
    gdouble xreal = 0.0, yreal = 0.0, xoff = 0.0, yoff = 0.0;
    gsize size, len;
    guint xres = 0, yres = 0, ncurves = 0, curveno, readbytes, i, j;
    gchar *channel = NULL, *segment = NULL, *end;
    gchar **curvenames = NULL;
    gint power10 = 0, *curvepower10 = NULL;
    GwyUnit *xyunit = NULL, **curveunits = NULL, *tmpunit;
    GwyLawn *lawn = NULL;

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

    str = g_string_new(NULL);
    p = buffer;
    if (size < 60 || memcmp(p, MAGIC, MAGIC_SIZE) != 0 || (p[MAGIC_SIZE] != '\n' && p[MAGIC_SIZE] != '\r')) {
        err_FILE_TYPE(error, "Gwyddion ASCII CMAP");
        goto fail;
    }
    p += MAGIC_SIZE;
    while (p - buffer < size && (*p == '\n' || *p == '\r'))
        p++;

    xyunit = gwy_unit_new(NULL);
    while ((len = read_a_few_lines(p, size - (p - buffer), str, 1))) {
        gchar *s = str->str;
        /* Simply re-read the current line if the header ends. */
        if (s[0] != '#')
            break;
        for (i = 1; i < str->len; i++) {
            if (!g_ascii_isspace(s[i]))
                break;
        }
        /* XXX: This invalidates str->len. The next GString function we use must be truncation to zero (or a similar
         * safe operation). */
        g_strstrip(s);
        s += i;
        if (strncmp(s, "Channel:", 8) == 0) {
            gwy_assign_string(&channel, s + 8);
            g_strstrip(channel);
        }
        else if (strncmp(s, "Rows:", 5) == 0)
            yres = atoi(s + 5);
        else if (strncmp(s, "Columns:", 8) == 0)
            xres = atoi(s + 8);
        else if (strncmp(s, "Curves:", 7) == 0) {
            if (ncurves) {
                err_INVALID(error, "Curves");
                goto fail;
            }
            ncurves = atoi(s + 7);
            curveunits = g_new(GwyUnit*, ncurves);
            curvepower10 = g_new0(gint, ncurves);
            curvenames = g_new0(gchar*, ncurves+1);
            for (i = 0; i < ncurves; i++)
                curveunits[i] = gwy_unit_new(NULL);
        }
        else if (strncmp(s, "Segment:", 8) == 0) {
            gwy_assign_string(&segment, s + 8);
            g_strstrip(segment);
        }
        else if (strncmp(s, "Width:", 6) == 0) {
            xreal = g_ascii_strtod(s + 6, &end);
            power10 = gwy_unit_set_from_string(xyunit, end);
            xreal *= gwy_exp10(power10);
        }
        else if (strncmp(s, "Height:", 7) == 0) {
            yreal = g_ascii_strtod(s + 7, &end);
            power10 = gwy_unit_set_from_string(xyunit, end);
            yreal *= gwy_exp10(power10);
        }
        else if (strncmp(s, "XOffset:", 8) == 0) {
            xoff = g_ascii_strtod(s + 8, &end);
            /* Do not override the xy unit by the offset unit. If someone puts different units to the fields trust
             * the dimensions. */
            tmpunit = gwy_unit_new_parse(end, &power10);
            xoff *= gwy_exp10(power10);
            g_clear_object(&tmpunit);
        }
        else if (strncmp(s, "YOffset:", 8) == 0) {
            yoff = g_ascii_strtod(s + 8, &end);
            /* Do not override the xy unit by the offset unit. If someone puts different units to the fields trust
             * the dimensions. */
            tmpunit = gwy_unit_new_parse(end, &power10);
            yoff *= gwy_exp10(power10);
            g_clear_object(&tmpunit);
        }
        else if (sscanf(s, "Curve%u:%n", &curveno, &readbytes) == 1) {
            if (!ncurves) {
                err_MISSING_FIELD(error, "Curves");
                goto fail;
            }
            if (!curveno || curveno > ncurves) {
                err_INVALID(error, "Curve");
                goto fail;
            }
            curveno--;
            gwy_assign_string(curvenames + curveno, s + readbytes);
        }
        else if (sscanf(s, "CurveUnit%u:%n", &curveno, &readbytes) == 1) {
            if (!ncurves) {
                err_MISSING_FIELD(error, "Curves");
                goto fail;
            }
            if (!curveno || curveno > ncurves) {
                err_INVALID(error, "CurveUnit");
                goto fail;
            }
            curveno--;
            curvepower10[curveno] = gwy_unit_set_from_string(curveunits[curveno], s + readbytes);
        }
        else {
            g_warning("Unparsed header line %s.", s);
        }
        p += len;
    }

    gwy_debug("dims %ux%u (%u curves)", xres, yres, ncurves);
    gwy_debug("real %gx%g", xreal, yreal);
    gwy_debug("channel %s, segment %s", channel, segment);

    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        goto fail;
    if (!ncurves) {
        err_MISSING_FIELD(error, "Curves");
        goto fail;
    }
    sanitise_real_size(&xreal, "xreal");
    sanitise_real_size(&yreal, "yreal");

    lawn = gwy_lawn_new(xres, yres, xreal, yreal, ncurves, 0);
    if (xoff)
        gwy_lawn_set_xoffset(lawn, xoff);
    if (yoff)
        gwy_lawn_set_yoffset(lawn, yoff);
    gwy_unit_assign(gwy_lawn_get_unit_xy(lawn), xyunit);
    for (curveno = 0; curveno < ncurves; curveno++) {
        gwy_unit_assign(gwy_lawn_get_unit_curve(lawn, curveno), curveunits[curveno]);
        if (curvenames[curveno])
            gwy_lawn_set_curve_label(lawn, curveno, curvenames[curveno]);
    }
    /* FIXME: What to do with the segment name? The text export does not preserve segmentation, so at most we know
     * the data were originally just one segment. */

    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++) {
            gdouble *data;
            gint nlines = ncurves, npoints = -1;

            if (!(len = read_a_few_lines(p, size - (p - buffer), str, ncurves))) {
                g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                            _("File is truncated."));
                goto fail;
            }
            if (!(data = gwy_parse_doubles(str->str, NULL,
                                           GWY_PARSE_DOUBLES_COMPLETELY | GWY_PARSE_DOUBLES_EMPTY_OK,
                                           &nlines, &npoints, NULL, &err))) {
                /* Handle empty curves correctly. */
                if (!npoints && !err) {
                    p += len;
                    continue;
                }
                err_PARSE_DOUBLES(error, &err);
                goto fail;
            }
            gwy_lawn_set_curves(lawn, j, i, npoints, data, NULL);
            g_free(data);

            p += len;
        }
    }

    file = gwy_file_new_in_construction();
    gwy_file_pass_cmap(file, 0, lawn);
    if (channel) {
        gwy_file_pass_title(file, GWY_FILE_CMAP, 0, channel);
        channel = NULL;
    }
    gwy_log_add_import(file, GWY_FILE_CMAP, 0, NULL, filename);

fail:
    gwy_unmap_file_contents(buffer);
    for (i = 0; i < ncurves; i++)
        g_object_unref(curveunits[i]);
    g_free(curveunits);
    g_free(curvepower10);
    g_strfreev(curvenames);
    g_object_unref(xyunit);
    g_string_free(str, TRUE);
    g_free(segment);
    g_free(channel);

    return file;
}

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