/*
 *  $Id: log.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2014-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.
 */

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

#include "libgwyddion/gwyddion.h"
#include "libgwyui/gwyui.h"

#include "libgwyapp/module-utils.h"
#include "libgwyapp/gwyapp.h"
#include "libgwyapp/internal.h"

static GwyStringList* get_data_log        (GwyFile *data,
                                           GQuark quark,
                                           gboolean create);
static gchar*         format_args         (const gchar *prefix);
static void           format_arg          (GQuark quark,
                                           GValue *gvalue,
                                           gpointer user_data);
static gboolean       find_settings_prefix(const gchar *function,
                                           const gchar *settings_name,
                                           GString *prefix);

static gboolean log_disabled = FALSE;

/* FIXME: We need an even fuller version, with source data kind and destination data kind and possibly different
 * source and destination files! It would also eliminate most cases when we need to explicitly pass the function
 * name because it would be the current function for the source data kind. */
/**
 * gwy_log_add_full:
 * @file: A data file container.
 * @data_kind: Type of the data object modified or created.
 * @previd: Identifier of the previous (source) data object of the same type in the container. Pass -1 for a no-source
 *          (or unclear source) operation or operations involving disparate data objects.
 * @newid: Identifier of the new (target) data object in the container.
 * @function: Quailified name of the function applied as shown by the module browser.  For instance
 *            "image::facet-level" or "tool::GwyToolCrop".
 * @...: Logging options as a %NULL-terminated list of pairs name, value.
 *
 * Adds an entry to the log of data processing operations for a channel.
 *
 * See the introduction for a description of valid @previd and @newid.
 *
 * It is possible to pass %NULL as @function.  In this case the log is just copied from source to target without
 * adding any entries. This can be useful to prevent duplicate log entries in modules that modify a data field and
 * then can also create secondary outputs. Note you still need to pass a second %NULL argument as the argument list
 * terminator.
 **/
void
gwy_log_add_full(GwyFile *file,
                 GwyDataKind data_kind,
                 gint previd,
                 gint newid,
                 const gchar *function,
                 ...)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(newid >= 0);

    if (log_disabled)
        return;

    va_list ap;
    va_start(ap, function);

    const gchar *key, *settings_name = NULL;
    while ((key = va_arg(ap, const gchar*))) {
        if (gwy_strequal(key, "settings-name")) {
            settings_name = va_arg(ap, const gchar*);
        }
        else {
            g_warning("Invalid logging option %s.", key);
            va_end(ap);
            return;
        }
    }
    va_end(ap);

    GString *str = NULL;
    if (function) {
        str = g_string_new(NULL);
        if (!find_settings_prefix(function, settings_name, str)) {
            g_string_free(str, TRUE);
            return;
        }
    }

    GwyFileKeyParsed parsed = { .data_kind = data_kind, .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
    GwyStringList *sourcelog = NULL;
    if (previd != -1) {
        parsed.id = previd;
        sourcelog = get_data_log(file, gwy_form_file_key(&parsed), FALSE);
    }

    parsed.id = newid;
    GQuark newquark = gwy_form_file_key(&parsed);
    GwyStringList *targetlog = NULL;

    if (newid == previd)
        targetlog = sourcelog;
    else
        targetlog = get_data_log(file, newquark, FALSE);

    if (targetlog && sourcelog && targetlog != sourcelog) {
        g_warning("Target log must not exist when replicating logs.");
        /* Fix the operation to simple log-append. */
        sourcelog = targetlog;
        previd = newid;
    }

    if (!targetlog) {
        if (sourcelog)
            targetlog = gwy_string_list_copy(sourcelog);
        else {
            if (!function)
                return;
            targetlog = gwy_string_list_new();
        }

        gwy_dict_pass_object(GWY_DICT(file), newquark, targetlog);
    }

    if (!function)
        return;

    GTimeVal t;
    g_get_current_time(&t);

    gchar *optime = g_time_val_to_iso8601(&t);
    gchar *s = strchr(optime, 'T');
    if (s)
        *s = ' ';

    gchar *args = format_args(str->str);
    g_string_printf(str, "%s(%s)@%s", function, args, optime);
    gwy_string_list_append_take(targetlog, g_string_free(str, FALSE));
    g_free(args);
    g_free(optime);
}

/**
 * gwy_log_add:
 * @file: A file data container.
 * @data_kind: Type of the data object.
 * @previd: Identifier of the previous (source) data object in the file. Pass -1 for a no-source (or unclear
 *          source) operation.
 * @newid: Identifier of the new (target) data object in the file.
 *
 * Adds an entry to the log of the current data processing operation.
 *
 * This simplified variant of gwy_log_add_full() takes the currently running data processing function name for the
 * corresponding data kind and constructs the qualified function name from that. The source and target data kinds
 * need to be the same and correspond to the function type. If the function creates data of a different kind than
 * the ‘natural’ kind for this type of function, you need gwy_log_add_full().
 *
 * Use gwy_log_add_import() to log data import from third party files and gwy_log_add_synth() to log data synthesis.
 **/
void
gwy_log_add(GwyFile *file,
            GwyDataKind data_kind,
            gint previd,
            gint newid)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(newid >= 0);

    const gchar *funcname = NULL, *prefix = NULL;

    if (data_kind == GWY_FILE_IMAGE) {
        funcname = gwy_image_func_current();
        prefix = "image";
    }
    else if (data_kind == GWY_FILE_GRAPH) {
        funcname = gwy_graph_func_current();
        prefix = "graph";
    }
    else if (data_kind == GWY_FILE_VOLUME) {
        funcname = gwy_volume_func_current();
        prefix = "volume";
    }
    else if (data_kind == GWY_FILE_XYZ) {
        funcname = gwy_xyz_func_current();
        prefix = "xyz";
    }
    else if (data_kind == GWY_FILE_CMAP) {
        funcname = gwy_cmap_func_current();
        prefix = "cmap";
    }
    else {
        g_return_if_reached();
    }

    g_return_if_fail(funcname);
    gchar *qname = g_strconcat(prefix, "::", funcname, NULL);
    gwy_log_add_full(file, data_kind, previd, newid, qname, NULL);
    g_free(qname);
}

/**
 * gwy_log_add_synth:
 * @file: A file data container.
 * @data_kind: Type of the data object.
 * @previd: Identifier of the initialisation data object in the file. Pass -1 if the data were generated from scatch,
 *          not by modification of existing data.
 * @newid: Identifier of the new data object in the file.
 *
 * Adds an entry to the log of the current synthetic data operation.
 *
 * This simplified variant of gwy_log_add_full() takes the currently running synthetic data function name for the
 * corresponding data kind and constructs the qualified function name from that.
 **/
void
gwy_log_add_synth(GwyFile *file,
                  GwyDataKind data_kind,
                  gint previd,
                  gint newid)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(newid >= 0);

    const gchar *funcname = gwy_synth_func_current();
    g_return_if_fail(funcname);

    gchar *qname = g_strconcat("synth", "::", funcname, NULL);
    gwy_log_add_full(file, data_kind, previd, newid, qname, NULL);
    g_free(qname);
}

/**
 * gwy_log_add_import:
 * @file: A data file container.
 * @data_kind: Type of the data object.
 * @id: Numerical id of a data in file.
 * @filetype: File type, i.e. the name of the function importing the data (without any "file::" prefix).
 *            It is possible to pass %NULL to fill the name of the currently running file type function automatically.
 * @filename: Name of the imported file.  If it is not valid UTF-8, it will be converted to UTF-8 using
 *            g_filename_to_utf8().  Failing even that, non-ASCII characters will be escaped.
 *
 * Logs the import of a data object from third-party file.
 *
 * This is a convenience wrapper for gwy_log_add_full().  The source id will be set to -1.  The file name will be
 * added to function arguments.
 **/
void
gwy_log_add_import(GwyFile *file,
                   GwyDataKind data_kind,
                   gint id,
                   const gchar *filetype,
                   const gchar *filename)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(filename);

    if (!filetype)
        filetype = gwy_file_func_current();
    g_return_if_fail(filetype);

    gchar *myfilename = NULL;

    if (g_utf8_validate(filename, -1, NULL))
        myfilename = g_strdup(filename);
    if (!myfilename)
        myfilename = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
    if (!myfilename)
        myfilename = g_strescape(filename, NULL);

    gchar *fskey = g_strdup_printf("/module/%s/filename", filetype);
    GQuark quark = g_quark_from_string(fskey);
    g_free(fskey);

    /* There should not be any settings key called
     * "/module/<filetype>/filename".
     * But just in case there is one, preserve it. */
    GValue savedval;
    gwy_clear1(savedval);
    GwyDict *settings = gwy_app_settings_get();
    if (gwy_dict_contains(settings, quark))
        savedval = gwy_dict_get_value(settings, quark);

    /* Eats myfilename. */
    gwy_dict_pass_string(settings, quark, myfilename);

    gchar *qualname = g_strconcat("file::", filetype, NULL);
    gwy_log_add_full(file, data_kind, -1, id, qualname, NULL);
    g_free(qualname);

    if (G_VALUE_TYPE(&savedval)) {
        gwy_dict_set_value(settings, quark, &savedval);
        g_value_unset(&savedval);
    }
    else
        gwy_dict_remove(settings, quark);
}

static GwyStringList*
get_data_log(GwyFile *file,
             GQuark quark,
             gboolean create)
{
    GwyStringList *slog = NULL;
    GwyDict *container = GWY_DICT(file);

    gwy_dict_gis_object(container, quark, &slog);
    if (slog || !create)
        return slog;

    slog = gwy_string_list_new();
    gwy_dict_pass_object(container, quark, slog);

    return slog;
}

static gchar*
format_args(const gchar *prefix)
{
    GwyDict *settings = gwy_app_settings_get();
    GPtrArray *values = g_ptr_array_new();
    gchar *retval;
    guint i;

    gwy_dict_foreach(settings, prefix, format_arg, values);
    g_ptr_array_add(values, NULL);
    retval = g_strjoinv(", ", (gchar**)values->pdata);
    for (i = 0; i < values->len-1; i++)
        g_free(g_ptr_array_index(values, i));
    g_ptr_array_free(values, TRUE);

    return retval;
}

static void
format_arg(GQuark quark, GValue *gvalue, gpointer user_data)
{
    GPtrArray *values = (GPtrArray*)user_data;
    gchar *formatted = NULL;
    const gchar *name = g_quark_to_string(quark);

    /* Do not store dialog window properties in the log. */
    if (strstr(name, "/dialog/")) {
        if (g_str_has_suffix(name, "/dialog/position/width")
            || g_str_has_suffix(name, "/dialog/position/height")
            || g_str_has_suffix(name, "/dialog/position/x")
            || g_str_has_suffix(name, "/dialog/position/y")
            || g_str_has_suffix(name, "/dialog/position/mconf"))
            return;
    }

    name = strrchr(name, '/');
    g_return_if_fail(name);
    name++;

    if (G_VALUE_HOLDS_DOUBLE(gvalue))
        formatted = g_strdup_printf("%s=%g", name, g_value_get_double(gvalue));
    else if (G_VALUE_HOLDS_INT(gvalue))
        formatted = g_strdup_printf("%s=%d", name, g_value_get_int(gvalue));
    else if (G_VALUE_HOLDS_INT64(gvalue))
        formatted = g_strdup_printf("%s=%" G_GINT64_FORMAT, name, g_value_get_int64(gvalue));
    else if (G_VALUE_HOLDS_BOOLEAN(gvalue))
        formatted = g_strdup_printf("%s=%s", name, g_value_get_boolean(gvalue) ? "True" : "False");
    else if (G_VALUE_HOLDS_STRING(gvalue)) {
        gchar *s = gwy_utf8_strescape(g_value_get_string(gvalue), NULL);
        formatted = g_strdup_printf("%s=\"%s\"", name, s);
        g_free(s);
    }
    else if (G_VALUE_HOLDS_UCHAR(gvalue)) {
        gint c = g_value_get_uchar(gvalue);
        if (g_ascii_isprint(c) && !g_ascii_isspace(c))
            formatted = g_strdup_printf("%s='%c'", name, c);
        else
            formatted = g_strdup_printf("%s=0x%02x", name, c);
    }
    else {
        g_warning("Cannot format argument of type %s.", g_type_name(G_VALUE_TYPE(gvalue)));
        return;
    }

    g_ptr_array_add(values, formatted);
}

static gboolean
find_settings_prefix(const gchar *function,
                     const gchar *settings_name,
                     GString *prefix)
{
    static const struct {
        const gchar *prefix;
        gboolean (*func_exists)(const gchar *name);
    }
    function_types[] = {
        { "image::",  &gwy_image_func_exists,  },
        { "file::",   &gwy_file_func_exists,   },
        { "graph::",  &gwy_graph_func_exists,  },
        { "volume::", &gwy_volume_func_exists, },
        { "xyz::",    &gwy_xyz_func_exists,    },
        { "cmap::",   &gwy_cmap_func_exists,   },
        { "synth::",  &gwy_synth_func_exists,  },
    };

    guint i;

    g_string_assign(prefix, "/module/");

    if (settings_name)
        g_string_append(prefix, settings_name);

    if (g_str_has_prefix(function, "builtin::")) {
        /* Ensure a non-existent prefix for builtins. */
        if (!settings_name)
            g_string_assign(prefix, "/__NO_SUCH_FUNCTION__");
        return TRUE;
    }

    for (i = 0; i < G_N_ELEMENTS(function_types); i++) {
        const gchar *ftpfx = function_types[i].prefix;

        if (g_str_has_prefix(function, ftpfx)) {
            const gchar *name = function + strlen(ftpfx);

            if (!function_types[i].func_exists(name)) {
                g_warning("Invalid %.*s function name %s.",
                          (gint)strlen(ftpfx)-2, ftpfx, name);
                return FALSE;
            }
            if (!settings_name)
                g_string_append(prefix, name);
            return TRUE;
        }
    }

    if (g_str_has_prefix(function, "tool::")) {
        const gchar *name = function + 6;
        GType type = g_type_from_name(name);
        GwyToolClass *klass;

        if (!type) {
            g_warning("Invalid tool name %s.", name);
            return FALSE;
        }

        if (!(klass = g_type_class_ref(type))) {
            g_warning("Invalid tool name %s.", name);
            return FALSE;
        }

        g_string_assign(prefix, klass->prefix);
        g_type_class_unref(klass);
        return TRUE;
    }

    g_warning("Invalid function name %s.", function);
    return FALSE;
}

/**
 * gwy_log_get_enabled:
 *
 * Reports whether logging of data processing operations is globally enabled.
 *
 * Returns: %TRUE if logging is enabled, %FALSE if it is disabled.
 **/
gboolean
gwy_log_get_enabled(void)
{
    return !log_disabled;
}

/**
 * gwy_log_set_enabled:
 * @setting: %TRUE to enable logging, %FALSE to disable it.
 *
 * Globally enables or disables logging of data processing operations.
 *
 * By default, logging is enabled.  Non-GUI applications that run module functions may wish to disable it.  Of course,
 * the log will presist only if the data container is saved into a GWY file.
 *
 * If logging is disabled logging functions such as gwy_log_add() become no-op.  It is possible to run the
 * log viewer with gwy_log_browser() to see log entries created when logging was enabled.
 **/
void
gwy_log_set_enabled(gboolean setting)
{
    log_disabled = !setting;
}

/**
 * SECTION: log
 * @title: Log
 * @short_description: Logging data processing operations
 *
 * The data processing operation log is a linear sequence of operations applied to a image, volume, XYZ or curve map
 * data.  The log is informative and not meant to capture all information necessary to reply the operations, even
 * though it can be sufficient for this purpose in simple cases.
 *
 * The log is a linear sequence.  This is only an approximation of the actual flow of information in the data
 * processing, which corresponds to an acyclic directed graph (not necessarily connected as data, masks and
 * presentations can have distinct sources).  The following rules thus apply to make it meaningful and useful.
 *
 * Each logging function takes two data identifiers: source and target. The source corresponds to the operation input,
 * the target corresponds to the data whose log is being updated.  The target may have already a log only if it is the
 * same as the source (which corresponds to simple data modification such as levelling or grain marking).  In all
 * other cases the target must not have a log yet – they represent the creation of new data either from scratch or
 * based on existing data (in the latter case the log of the existing data is replicated to the new one).
 *
 * Complex multi-data operations are approximated by one of the simple operations.  For instance, data arithmetic can
 * be treated as the construction of a new channel from scratch as it is unclear which input data the output channel
 * is actually based on, if any at all.  Modifications that use data from other channels, such as masking using
 * another data or tip convolution, should be represented as simple modifications of the primary channel.
 *
 * Logging functions such as gwy_log_add() take settings values corresponding to the function name and
 * store them in the log entry. If the settings are stored under a different name, use the "settings-name" logging
 * option to set the correct name.
 **/

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