/*
 *  $Id: app.c 30094 2026-06-21 11:11:50Z yeti-dn $
 *  Copyright (C) 2003-2026 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 <stdarg.h>
#include <math.h>
#include <glib/gi18n-lib.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

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

#include "libgwyapp/internal.h"

static GtkWindow     *main_window      = NULL;
static GtkAccelGroup *main_accel_group = NULL;
static GwyTool       *current_tool     = NULL;

static gboolean save_main_window_position   (void);
static void     restore_main_window_position(void);
static void     monitor_config_checksum     (GtkWidget *widget,
                                             gchar *buf17);

static G_DEFINE_QUARK(gwy-app-tree-model-kind, model_kind)

/********************************************************************************************************************
 *                                                                                                                  *
 *     Stubs FIXME GTK3                                                                                             *
 *                                                                                                                  *
 ********************************************************************************************************************/

void
gwy_app_gl_material_editor(void)
{
}

/**
 * gwy_app_quit:
 *
 * Quits the application.
 *
 * This function may present a confirmation dialog to the user and it may let the application to continue running.  If
 * it quits the application, it performs some shutdown actions and then quits the Gtk+ main loop with gtk_main_quit().
 *
 * Returns: Always %TRUE to be usable as an event handler.  However, if the application is actually terminated, this
 *          function does not return.
 **/
gboolean
gwy_app_quit(void)
{
    gwy_data_browser_shut_down();
    save_main_window_position();
    gwy_data_browser_shut_down();
    g_clear_object(&current_tool);
    /* XXX: EXIT-CLEAN-UP */
    gtk_widget_destroy(GTK_WIDGET(main_window));
    g_object_unref(gwy_app_sensitivity_get_group());

    gtk_main_quit();
    return TRUE;
}

static gboolean
save_main_window_position(void)
{
    gwy_app_save_window_position(GTK_WINDOW(main_window), "/app/toolbox", TRUE, TRUE);
    return FALSE;
}

static void
restore_main_window_position(void)
{
    gwy_app_restore_window_position(GTK_WINDOW(main_window), "/app/toolbox", FALSE);
}

/**
 * gwy_app_main_accel_group:
 *
 * Obtains the main application accelerator group.
 *
 * The group includes accelerators for terminating Gwyddion, opening and saving files and similar operations. You
 * should add it to other windows where these global keyboard shortcuts should work.
 *
 * If the main accelerator group does not exist yet, it will be created.
 *
 * Return: (transfer none): The main accelerator group.
 **/
GtkAccelGroup*
gwy_app_main_accel_group(void)
{
    if (!main_accel_group)
        main_accel_group = gtk_accel_group_new();
    return main_accel_group;
}

/**
 * gwy_app_main_window:
 *
 * Returns the main application window.
 *
 * In a running Gwyddion GUI, the main window is always the toolbox. Technically, the function returns whatever was
 * set with gwy_app_set_main_window() during program startup. Including possibly %NULL is nothing has been set.
 *
 * Returns: (transfer none) (nullable): The main window.
 **/
GtkWindow*
gwy_app_main_window(void)
{
    return main_window;
}

/**
 * gwy_app_set_main_window:
 * @window: A window, presumably the Gwyddion toolbox.
 *
 * Sets the main application window.
 *
 * This function is probably only useful in Gwyddion itself and should be ignored.
 *
 * It should to be called exactly once at Gwyddion startup. It makes gwy_app_main_window() work and adds the main
 * accelerator group to the window. It also restores the main window position and size, so it really makes sense for
 * the Gwyddion toolbox.
 **/
void
gwy_app_set_main_window(GtkWindow *window)
{
    g_return_if_fail(GTK_IS_WINDOW(window));
    if (main_window && window != main_window) {
        g_critical("The main window was already set to %p.", main_window);
        return;
    }

    main_window = window;
    gtk_window_add_accel_group(window, gwy_app_main_accel_group());
    restore_main_window_position();
    g_signal_connect(window, "delete-event", G_CALLBACK(save_main_window_position), NULL);
    g_signal_connect(window, "show", G_CALLBACK(restore_main_window_position), NULL);
}

/**
 * gwy_app_switch_tool:
 * @toolname: Tool name, that is type name of the tool object in the GLib type system.  This can be for instance
 *            "GwyToolGrainMeasure".
 *
 * Switches the current tool to given tool.
 *
 * If the tool is already active it is shown when hidden and hidden when
 * visible.
 **/
void
gwy_app_switch_tool(const gchar *toolname)
{
    GwyDict *settings;
    GwyTool *newtool;
    GwyDataView *data_view;
    GType type;
    gboolean do_restore = FALSE;

    gwy_debug("%s", toolname ? toolname : "NONE");
    type = g_type_from_name(toolname);
    g_return_if_fail(type);

    gwy_data_browser_get_current(GWY_APP_DATA_VIEW, &data_view, 0);
    if (current_tool && type == G_TYPE_FROM_INSTANCE(current_tool)) {
        if (!gwy_tool_is_visible(current_tool))
            gwy_tool_show(current_tool);
        else
            gwy_tool_hide(current_tool);
        return;
    }

    g_clear_object(&current_tool);
    newtool = (GwyTool*)g_object_new(type, NULL);
    current_tool = newtool;
    g_return_if_fail(GWY_IS_TOOL(newtool));

    settings = gwy_app_settings_get();
    gwy_dict_gis_boolean_by_name(settings, "/app/restore-tool-position", &do_restore);
    if (do_restore)
        gwy_tool_restore_screen_position(newtool);

    if (data_view) {
        GwySpectra *spectra;

        gwy_tool_data_switched(current_tool, data_view);
        gwy_data_browser_get_current(GWY_APP_SPECTRA, &spectra, 0);
        gwy_tool_spectra_switched(current_tool, spectra);
        gwy_tool_show(current_tool);
    }
}

/**
 * gwy_app_current_tool_name:
 *
 * Obtains the name of currently active tool.
 *
 * See gwy_app_switch_tool() for the name description.
 *
 * In some rare circumstances, this function can return %NULL because no tool is active.  This includes program
 * startup and shutdown and during the construction of a new #GwyTool object while switching tools.  Also, %NULL is
 * typically returned outside Gwyddion in programs just using the libraries.
 *
 * Returns: The tool name.
 **/
const gchar*
gwy_app_current_tool_name(void)
{
    if (!current_tool)
        return NULL;

    return G_OBJECT_TYPE_NAME(current_tool);
}

/**
 * gwy_app_current_tool:
 *
 * Obtains the currently active tool object.
 *
 * In some rare circumstances, this function can return %NULL because no tool is active.  This includes program
 * startup and shutdown and during the construction of a new #GwyTool object while switching tools.  Also, %NULL is
 * typically returned outside Gwyddion in programs just using the libraries.
 *
 * Returns: The current tool object.
 **/
GwyTool*
gwy_app_current_tool(void)
{
    return current_tool;
}

// Make a string key prefix of the form "/<PREFIX>/position/<CHECKSUM>", but large enough to hold the complete string
// keys (not just the prefix). Return the prefix in @len.
static gchar*
make_window_position_key_buffer(const gchar *prefix, const gchar *mconf_checksum, guint *len)
{
    guint pos_len = strlen("/position");
    guint prefix_len = strlen(prefix);
    guint mconf_len = strlen(mconf_checksum); // Should always be 16, but whatever.
    gchar *key = g_new(gchar, prefix_len + pos_len + mconf_len + sizeof("/monitor") + 1);  // The longest suffix.
    guint keypos = 0;
    gwy_assign(key + keypos, prefix, prefix_len);
    keypos += prefix_len;
    gwy_assign(key + keypos, "/position", pos_len);
    keypos += pos_len;
    key[keypos++] = '/';
    gwy_assign(key + keypos, mconf_checksum, mconf_len);
    keypos += mconf_len;
    key[keypos] = '\0';
    *len = keypos;

    return key;
}

static const gchar*
describe_monitor(GdkMonitor *monitor)
{
    const gchar *manufact = monitor ? gdk_monitor_get_manufacturer(monitor) : NULL;
    const gchar *model = monitor ? gdk_monitor_get_model(monitor) : NULL;
    GdkRectangle monitor_geom = { 0, 0, 0, 0 };
    if (monitor)
        gdk_monitor_get_geometry(monitor, &monitor_geom);
    gchar *s = g_strdup_printf("%s::%s::%dx%d",
                               manufact ? manufact : "Unknown",
                               model ? model : "Unknown",
                               monitor_geom.width, monitor_geom.height);
    const gchar *retval = g_intern_string(s);
    g_free(s);
    return retval;
}

static GdkMonitor*
find_monitor(GtkWidget *widget, const gchar *desc)
{
    GdkDisplay *display = gtk_widget_get_display(widget);
    if (!display)
        return NULL;

    if (!desc)
        return NULL;

    gint n = gdk_display_get_n_monitors(display);
    for (gint i = 0; i < n; i++) {
        GdkMonitor *monitor = gdk_display_get_monitor(display, i);
        if (gwy_strequal(describe_monitor(monitor), desc))
            return monitor;
    }
    return NULL;
}

/**
 * gwy_app_save_window_position:
 * @window: A window to save position of.
 * @prefix: Unique prefix in settings to store the information under.
 * @position: %TRUE to save position information.
 * @size: %TRUE to save size information.
 *
 * Saves position and/or size of a window to settings.
 *
 * Some sanity checks are included, therefore if window position and/or size is too suspicious, it is not saved.
 **/
void
gwy_app_save_window_position(GtkWindow *window,
                             const gchar *prefix,
                             gboolean position,
                             gboolean size)
{
    g_return_if_fail(GTK_IS_WINDOW(window));
    g_return_if_fail(prefix);
    if (!(position || size))
        return;

    gwy_debug("saving window %s (pos=%d, size=%d)", prefix, position, size);
    GtkWidget *widget = GTK_WIDGET(window);
    gchar mconf_checksum[17];
    monitor_config_checksum(widget, mconf_checksum);
    gwy_debug("mcof = %s", mconf_checksum);

    guint len;
    gchar *key = make_window_position_key_buffer(prefix, mconf_checksum, &len);
    GwyDict *settings = gwy_app_settings_get();
    gint scw, sch;
    gwy_get_screen_size(widget, &scw, &sch);

    /* FIXME: read the gtk_window_get_position() docs about how this is a broken approach. But it is better than
     * nothing. We could detect broken environments like Wayland which do not support programmatic positioning of
     * windows at all and avoid saving the poisition. On the other hand, gtk_window_move() should also be noop then
     * so we do not care. */
    if (position) {
        GdkWindow *gdkwin = gtk_widget_get_window(widget);
        gint x, y;
        gtk_window_get_position(window, &x, &y);

        GdkDisplay *display = gtk_widget_get_display(widget);
        GdkMonitor *monitor = (display ? gdk_display_get_monitor_at_window(display, gdkwin) : NULL);
        const gchar *desc = describe_monitor(monitor);

        GdkRectangle monitor_geom = { 0, 0, 0, 0 };
        if (monitor)
            gdk_monitor_get_geometry(monitor, &monitor_geom);
        x -= monitor_geom.x;
        y -= monitor_geom.y;

        gwy_debug("desc: %s (%d, %d)", desc, x, y);
        if (x >= 0 && y >= 0 && x+1 < scw && y+1 < sch) {
            strcpy(key + len, "/x");
            gwy_dict_set_int32_by_name(settings, key, x);
            strcpy(key + len, "/y");
            gwy_dict_set_int32_by_name(settings, key, y);
            strcpy(key + len, "/monitor");
            gwy_dict_set_string_by_name(settings, key, desc);
        }
    }
    if (size) {
        gint w, h;
        gtk_window_get_size(window, &w, &h);
        if (w > 1 && h > 1) {
            strcpy(key + len, "/width");
            gwy_dict_set_int32_by_name(settings, key, w);
            strcpy(key + len, "/height");
            gwy_dict_set_int32_by_name(settings, key, h);
        }
    }
    g_free(key);
}

static gboolean
get_window_int_for_mconf(GwyDict *settings,
                         gchar *key, guint prefix_len, const gchar *suffix,
                         gint *value)
{
    strcpy(key + prefix_len, suffix);
    if (gwy_dict_gis_int32_by_name(settings, key, value)) {
        gwy_debug("%s: %d", key, *value);
        return TRUE;
    }
    gwy_debug("no %s", key);
    return FALSE;
}

/**
 * gwy_app_restore_window_position:
 * @window: A window to restore position of.
 * @prefix: Unique prefix in settings to get the information from (the same as
 *          in gwy_app_save_window_position()).
 * @grow_only: %TRUE to only attempt set the window default size bigger than it
 *              requests, never smaller.
 *
 * Restores a window position and/or size from settings.
 *
 * Unlike gwy_app_save_window_position(), this function has no @position and @size arguments, it simply restores all
 * attributes that were saved.
 *
 * Note to restore position (not size) it should be called twice for each window to accommodate sloppy window
 * managers: once before the window is shown, second time immediately after showing the window.
 *
 * Some sanity checks are included, therefore if saved window position and/or size is too suspicious, it is not
 * restored.
 **/
void
gwy_app_restore_window_position(GtkWindow *window,
                                const gchar *prefix,
                                gboolean grow_only)
{
    g_return_if_fail(GTK_IS_WINDOW(window));
    g_return_if_fail(prefix);

    GtkWidget *widget = GTK_WIDGET(window);
    gchar mconf_checksum[17];
    monitor_config_checksum(widget, mconf_checksum);
    gwy_debug("mcof = %s", mconf_checksum);

    guint len;
    gchar *key = make_window_position_key_buffer(prefix, mconf_checksum, &len);
    GwyDict *settings = gwy_app_settings_get();
    gint scw, sch;
    gwy_get_screen_size(widget, &scw, &sch);
    gint x = -1, y = -1, w = -1, h = -1;
    const gchar *desc = NULL;
    get_window_int_for_mconf(settings, key, len, "/x", &x);
    get_window_int_for_mconf(settings, key, len, "/y", &y);
    get_window_int_for_mconf(settings, key, len, "/width", &w);
    get_window_int_for_mconf(settings, key, len, "/height", &h);
    strcpy(key + len, "/monitor");
    gwy_dict_gis_string_by_name(settings, key, &desc);
    gwy_debug("%s: %s", key, desc);

    gwy_debug("window %dx%d at (%d,%d)", w, h, x, y);
    if (x >= 0 && y >= 0 && x+1 < scw && y+1 < sch) {
        GdkMonitor *monitor = find_monitor(widget, desc);
        GdkRectangle monitor_geom = { 0, 0, 0, 0 };
        if (monitor)
            gdk_monitor_get_geometry(monitor, &monitor_geom);
        gwy_debug("moving window %p(%s) to (%d, %d) at monitor %s(+%d,+%d)",
                  window, gtk_window_get_title(window), x, y, desc, monitor_geom.x, monitor_geom.y);
        gtk_window_move(window, x + monitor_geom.x, y + monitor_geom.y);
    }
    if (w > 1 && h > 1) {
        if (grow_only) {
            GtkRequisition req;
            gtk_widget_get_preferred_size(widget, NULL, &req);
            w = MAX(w, req.width);
            h = MAX(h, req.height);
        }
        gtk_window_set_default_size(window, w, h);
    }

    g_free(key);
}

/* We probably do not have to this repeatedly but GDK has the monitor properties tabulated so it is just a few extra
 * function calls... */
static void
monitor_config_checksum(GtkWidget *widget,
                        gchar *buf17)
{
    static GChecksum *checksummer = NULL;
    const gchar *hexstr = "0000000000000000";

    if (!widget)
        goto end;

    GdkDisplay *display = gtk_widget_get_display(widget);
    if (!display)
        display = gdk_display_get_default();
    if (!display)
        goto end;

    gint n = gdk_display_get_n_monitors(display);
    if (!n)
        goto end;

    if (!checksummer) {
        checksummer = g_checksum_new(G_CHECKSUM_MD5);
        g_assert(g_checksum_type_get_length(G_CHECKSUM_MD5) == 16);
    }
    else
        g_checksum_reset(checksummer);

    for (gint i = 0; i < n; i++) {
        GdkMonitor *monitor = gdk_display_get_monitor(display, i);
        const gchar *desc = describe_monitor(monitor);
        g_checksum_update(checksummer, desc, strlen(desc)+1);
    }
    hexstr = g_checksum_get_string(checksummer);

end:
    gwy_assign(buf17, hexstr, 16);
    buf17[16] = '\0';
}

gint
_gwy_app_get_n_recent_files(void)
{
    return 10;
}

/**
 * gwy_app_get_tree_model_kind:
 * @model: A tree model.
 *
 * Identifies the kind of tree model.
 *
 * Receivers of tree model drag data can use this function to see if the source is a known Gwyddion DnD data source.
 * The known kinds include data browser lists "image", "graph", "spectra", "volume", "xyz" and "cmap". However, these
 * lists store only to internal data browser structures, making them useful drag sources only within libgwyapp itself.
 *
 * Graph window curve lists are of "graph-curves" kind and the selection manager tool selection list is
 * "image-selections".
 *
 * Returns: (nullable): String identifier of the tree model kind. It may be %NULL if the drag source is not identified
 *                      as any standard kind.
 **/
const gchar*
gwy_app_get_tree_model_kind(GtkTreeModel *model)
{
    return g_object_get_qdata(G_OBJECT(model), model_kind_quark());
}

/**
 * gwy_app_set_tree_model_kind:
 * @model: A tree model.
 * @kind: (nullable): String identifier of the tree model kind.
 *
 * Sets the kind of a tree model.
 *
 * You should almost never need this function because all standard tree models usable as drag sources set the kind
 * themselves. See also gwy_app_get_tree_model_kind().
 **/
void
gwy_app_set_tree_model_kind(GtkTreeModel *model,
                            const gchar *kind)
{
    g_object_set_qdata(G_OBJECT(model), model_kind_quark(), (gpointer)kind);
}

/**
 * SECTION: app
 * @title: Application
 * @short_description: Core application interface, window management
 **/

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