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

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

#include "libgwyddion/macros.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/dict.h"
#include "libgwyui/utils.h"

#include "libgwyapp/app.h"
#include "libgwyapp/help.h"
#include "libgwyapp/log.h"
#include "libgwyapp/log-browser.h"
#include "libgwyapp/module-utils.h"
#include "libgwyapp/internal.h"

enum {
    LOG_TYPE,
    LOG_FUNCNAME,
    LOG_PARAMETERS,
    LOG_TIME,
};

struct _GwyLogBrowserPrivate {
    GwyFile *file;
    GwyDataKind data_kind;
    gint id;

    GwyNullStore *store;
    GwyStringList *log;
    GString *buf;
    gulong changed_id;
    GtkWidget *treeview;
    GtkWidget *save;
    GtkWidget *clear;
    GtkWidget *close;
};

static void           finalize          (GObject *object);
static void           destroyed         (GtkWidget *widget);
static GtkWidget*     create_treeview   (GwyLogBrowser *browser);
static GtkWidget*     create_buttons    (GwyLogBrowser *browser);
static void           render_log_cell   (GtkTreeViewColumn *column,
                                         GtkCellRenderer *renderer,
                                         GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         gpointer user_data);
static void           update_title      (GwyLogBrowser *browser);
static void           update_sensitivity(GwyLogBrowser *browser);
static void           export_log        (GwyLogBrowser *browser);
static void           clear_log         (GwyLogBrowser *browser);
static void           log_changed       (GwyStringList *slog,
                                         GwyLogBrowser *browser);
static void           data_finalized    (gpointer user_data,
                                         GObject *where_the_object_was);
static GwyStringList* get_data_log      (GwyFile *file,
                                         GwyDataKind data_kind,
                                         gint id,
                                         gboolean create);

static GObjectClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyLogBrowser, gwy_log_browser, GTK_TYPE_WINDOW,
                        G_ADD_PRIVATE(GwyLogBrowser))

static void
gwy_log_browser_class_init(GwyLogBrowserClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_log_browser_parent_class;

    object_class->finalize = finalize;

    widget_class->destroy = destroyed;
}

static void
finalize(GObject *object)
{
    GwyLogBrowser *browser = GWY_LOG_BROWSER(object);
    GwyLogBrowserPrivate *priv = browser->priv;

    g_clear_object(&priv->store);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
gwy_log_browser_init(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv;

    browser->priv = priv = gwy_log_browser_get_instance_private(browser);

    GtkWidget *widget = GTK_WIDGET(browser);
    GtkWindow *window = GTK_WINDOW(browser);

    priv->buf = g_string_new(NULL);
    GwyNullStore *store = priv->store = gwy_null_store_new(0);
    priv->treeview = create_treeview(browser);
    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
    gtk_tree_view_set_model(treeview, GTK_TREE_MODEL(store));

    GtkRequisition request;
    gtk_widget_get_preferred_size(priv->treeview, NULL, &request);
    request.width = MAX(request.width, 320);
    request.height = MAX(request.height, 400);
    gint scw, sch;
    gwy_get_screen_size(widget, &scw, &sch);
    gtk_window_set_default_size(window, MIN(request.width + 24, 2*scw/3), MIN(request.height + 32, 2*sch/3));
    gtk_window_add_accel_group(window, gwy_app_main_accel_group());

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(browser), vbox);

    GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
    gtk_container_add(GTK_CONTAINER(scroll), priv->treeview);

    gtk_box_pack_start(GTK_BOX(vbox), create_buttons(browser), FALSE, FALSE, 0);

    gwy_help_add_to_window(window, "logging", NULL, GWY_HELP_DEFAULT);

    gtk_widget_show_all(vbox);
}

/**
 * gwy_log_browser_new:
 * @file: A data file container.
 * @data_kind: Type of data item.
 * @id: Id of data item in @data to show the log for.
 *
 * Creates a new data processing operation log browser for a data object in a file.
 *
 * If there is currently no log for given data, a new empty log is created, unless logging is currently disabled.
 *
 * Returns: (transfer full): A new metadata browser window.
 **/
GtkWidget*
gwy_log_browser_new(GwyFile *file,
                    GwyDataKind data_kind,
                    gint id)
{
    GwyLogBrowser *browser = g_object_new(GWY_TYPE_LOG_BROWSER, NULL);
    GwyLogBrowserPrivate *priv = browser->priv;

    g_return_val_if_fail(GWY_IS_FILE(file), (GtkWidget*)browser);
    g_return_val_if_fail((guint)data_kind < GWY_FILE_N_KINDS, (GtkWidget*)browser);
    g_return_val_if_fail(id >= 0, (GtkWidget*)browser);

    priv->file = file;
    priv->data_kind = data_kind;
    priv->id = id;

    if (!gwy_dict_contains(GWY_DICT(file), gwy_file_key_data(data_kind, id))) {
        g_warning("Creating log browser for non-existent data.");
    }

    GwyStringList *slog = priv->log = get_data_log(file, data_kind, id, gwy_log_get_enabled());
    if (slog) {
        g_object_weak_ref(G_OBJECT(slog), data_finalized, browser);
        priv->changed_id = g_signal_connect(slog, "value-changed", G_CALLBACK(log_changed), browser);
        gwy_null_store_set_n_rows(priv->store, gwy_string_list_get_length(slog));
    }

    update_title(browser);
    update_sensitivity(browser);

    return (GtkWidget*)browser;
}

static GtkWidget*
create_treeview(GwyLogBrowser *browser)
{
    static const GwyEnum columns[] = {
        { N_("Type"),       LOG_TYPE,       },
        { N_("Function"),   LOG_FUNCNAME,   },
        { N_("Parameters"), LOG_PARAMETERS, },
        { N_("Time"),       LOG_TIME,       },
    };

    GtkWidget *widget = gtk_tree_view_new();
    GtkTreeView *treeview = GTK_TREE_VIEW(widget);

    for (guint i = 0; i < G_N_ELEMENTS(columns); i++) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_(columns[i].name), renderer, NULL);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_log_cell, browser, NULL);
        gtk_tree_view_append_column(treeview, column);
        g_object_set_data(G_OBJECT(renderer), "column", GUINT_TO_POINTER(columns[i].value));

        if (columns[i].value == LOG_PARAMETERS) {
            gtk_tree_view_column_set_expand(column, TRUE);
            g_object_set(renderer,
                         "ellipsize", PANGO_ELLIPSIZE_END,
                         "ellipsize-set", TRUE,
                         NULL);
        }
    }

    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_NONE);

    return widget;
}

static GtkWidget*
create_buttons(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);

    priv->save = gwy_create_stock_button(GWY_STOCK_EXPORT);
    gtk_box_pack_start(GTK_BOX(hbox), priv->save, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->save, "clicked", G_CALLBACK(export_log), browser);

    priv->clear = gwy_create_stock_button(GWY_STOCK_CLEAR);
    gtk_box_pack_start(GTK_BOX(hbox), priv->clear, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->clear, "clicked", G_CALLBACK(clear_log), browser);

    priv->close = gwy_create_stock_button(GWY_STOCK_CLOSE);
    gtk_box_pack_start(GTK_BOX(hbox), priv->close, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->close, "clicked", G_CALLBACK(gtk_widget_destroy), browser);

    return hbox;
}

static void
render_log_cell(G_GNUC_UNUSED GtkTreeViewColumn *column,
                GtkCellRenderer *renderer,
                GtkTreeModel *model,
                GtkTreeIter *iter,
                gpointer user_data)
{
    GwyLogBrowser *browser = (GwyLogBrowser*)user_data;
    GwyLogBrowserPrivate *priv = browser->priv;
    guint id = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(renderer), "column"));

    guint i;
    gtk_tree_model_get(model, iter, 0, &i, -1);
    const gchar *s = gwy_string_list_get(priv->log, i);
    g_return_if_fail(s);

    GString *buf = priv->buf;
    g_string_truncate(buf, 0);
    if (id == LOG_TYPE) {
        const gchar *t = strstr(s, "::");
        g_return_if_fail(t);
        g_string_append_len(buf, s, t-s);
    }
    else if (id == LOG_FUNCNAME) {
        const gchar *t = strstr(s, "::");
        g_return_if_fail(t);
        s = t+2;
        t = strchr(s, '(');
        g_return_if_fail(t);
        g_string_append_len(buf, s, t-s);
    }
    else if (id == LOG_PARAMETERS) {
        const gchar *t = strchr(s, '(');
        g_return_if_fail(t);
        s = t+1;
        t = strrchr(s, ')');
        g_return_if_fail(t);
        g_string_append_len(buf, s, t-s);
    }
    else if (id == LOG_TIME) {
        const gchar *t = strrchr(s, '@');
        g_return_if_fail(t);
        g_string_append(buf, t+1);
    }

    g_object_set(renderer, "text", buf->str, NULL);
}

static void
export_log(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;

    guint n = gwy_string_list_get_length(priv->log);
    const gchar **entries = g_new(const gchar*, n+2);
    for (guint i = 0; i < n; i++)
        entries[i] = gwy_string_list_get(priv->log, i);
    entries[n] = "";
    entries[n+1] = NULL;

    gchar *str_to_save = g_strjoinv("\n", (gchar**)entries);
    g_free(entries);

    gwy_save_auxiliary_data(_("Export Log"), GTK_WINDOW(browser), str_to_save, -1, FALSE);

    g_free(str_to_save);
}

static void
clear_log(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;

    if (priv->log)
        gwy_string_list_clear(priv->log);
}

static void
log_changed(GwyStringList *slog, GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;
    GwyNullStore *store = priv->store;
    guint n = gwy_string_list_get_length(slog);

    // The log can be only:
    // - extended with new entries (data processing, redo)
    // - truncated (undo)
    // - cleared (clear)
    // In all cases simple gwy_null_store_set_n_rows() does the right thing.
    gwy_null_store_set_n_rows(store, n);
    update_sensitivity(browser);
}

static void
update_title(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;
    gchar *dataname = gwy_file_get_display_title(priv->file, priv->data_kind, priv->id);
    gchar *title = g_strdup_printf(_("Log of %s (%s)"), dataname, g_get_application_name());
    gtk_window_set_title(GTK_WINDOW(browser), title);
    g_free(title);
    g_free(dataname);
}

static void
update_sensitivity(GwyLogBrowser *browser)
{
    GwyLogBrowserPrivate *priv = browser->priv;
    gtk_widget_set_sensitive(priv->save, !!gwy_null_store_get_n_rows(priv->store));
}

static void
destroyed(GtkWidget *widget)
{
    GwyLogBrowser *browser = GWY_LOG_BROWSER(widget);
    GwyLogBrowserPrivate *priv = browser->priv;

    GWY_FREE_STRING(priv->buf);
    if (priv->log) {
        g_clear_signal_handler(&priv->changed_id, priv->log);
        g_object_weak_unref(G_OBJECT(priv->log), data_finalized, browser);
        priv->log = NULL;
    }

    GTK_WIDGET_CLASS(parent_class)->destroy(widget);
}

static void
data_finalized(gpointer user_data,
               G_GNUC_UNUSED GObject *where_the_object_was)
{
    GwyLogBrowser *browser = (GwyLogBrowser*)user_data;
    browser->priv->log = NULL;
    gtk_widget_destroy(GTK_WIDGET(browser));
}

static GwyStringList*
get_data_log(GwyFile *file,
             GwyDataKind data_kind,
             gint id,
             gboolean create)
{
    GwyStringList *slog = NULL;
    GwyDict *container = GWY_DICT(file);
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);

    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;
}

/**
 * SECTION: log-browser
 * @title: Log browser
 * @short_description: Display data processing history
 **/

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