/*
 *  $Id: settings.c 30115 2026-06-22 15:10:37Z yeti-dn $
 *  Copyright (C) 2003-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 <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/serializable.h"
#include "libgwyui/gwyui.h"

#include "libgwyapp/log.h"
#include "libgwyapp/settings.h"

static gboolean create_config_dir_real       (const gchar *cfgdir,
                                              GError **error);
static void     gwy_app_settings_set_defaults(GwyDict *settings);
static void     gwy_app_settings_gather      (GwyDict *settings);
static void     gwy_app_settings_apply       (GwyDict *settings);

static const GwyRGBA default_mask_color = { 1.0, 0.0, 0.0, 0.5 };
static const gchar *magic_header = "Gwyddion Settings 1.0\n";

static GwyDict *gwy_settings = NULL;

/**
 * gwy_app_settings_get:
 *
 * Gets the Gwyddion settings.
 *
 * The program settings are in a #GwyDict, which is normally loaded at program startup using a function like
 * gwy_app_init() and saved at exit using a function like gwy_app_settings_save(). If you use settings, you should
 * load resources beforehand. Or, better, use gwy_app_init().
 *
 * For storing persistent module data manually you should use <literal>"/module/YOUR_MODULE_NAME/"</literal> prefix.
 * However, in common cases you should use #GwyParamDef and #GwyParams which can handle the dirty work themselves.
 *
 * Returns: The settings as a #GwyDict.
 **/
GwyDict*
gwy_app_settings_get(void)
{
    if (!gwy_settings) {
        gwy_settings = GWY_DICT(gwy_dict_new());
        gwy_app_settings_set_defaults(gwy_settings);
        gwy_app_settings_apply(gwy_settings);
    }

    return gwy_settings;
}

/**
 * gwy_app_settings_free:
 *
 * Frees Gwyddion settings.
 *
 * Should not be called only by main application.
 **/
void
gwy_app_settings_free(void)
{
    g_clear_object(&gwy_settings);
}

/**
 * gwy_app_settings_save:
 * @filename: A filename to save the settings to.
 * @error: Location to store loading error to, or %NULL.
 *
 * Saves the settings.
 *
 * Use gwy_app_settings_get_settings_filename() to obtain a suitable default filename.
 *
 * Returns: Whether it succeeded.
 **/
gboolean
gwy_app_settings_save(const gchar *filename,
                      GError **error)
{
    GwyDict *settings;
    GPtrArray *pa;
    guint i;
    gboolean ok;
    FILE *fh;
    gchar *cfgdir;

    cfgdir = g_path_get_dirname(filename);
    ok = create_config_dir_real(cfgdir, error);
    g_free(cfgdir);
    if (!ok)
        return FALSE;

    gwy_debug("Saving text settings to `%s'", filename);
    settings = gwy_app_settings_get();
    gwy_app_settings_gather(settings);
    g_return_val_if_fail(GWY_IS_DICT(settings), FALSE);
    fh = gwy_fopen(filename, "w");
    if (!fh) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_FILE,
                    _("Cannot open file for writing: %s."), g_strerror(errno));
        return FALSE;
    }
    if (fputs(magic_header, fh) == EOF) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_FILE,
                    _("Cannot write to file: %s."), g_strerror(errno));
        fclose(fh);
        return FALSE;
    }

    ok = TRUE;
    pa = gwy_dict_serialize_to_text(settings);
    for (i = 0; i < pa->len; i++) {
        if (fputs((gchar*)pa->pdata[i], fh) == EOF) {
            g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_FILE,
                        _("Cannot write to file: %s."), g_strerror(errno));
            ok = FALSE;
            break;
        }
        fputc('\n', fh);
    }
    g_ptr_array_free(pa, TRUE);
    fclose(fh);

    return ok;
}

/**
 * gwy_app_settings_load:
 * @filename: A filename to read settings from.
 * @error: Location to store loading error to, or %NULL.
 *
 * Initialises settings by loading a settings file.
 *
 * Any existing settings are discarded. Consider gwy_app_settings_merge_string() for piecewise settings creation.
 *
 * Returns: Whether it succeeded.  In either case you can call gwy_app_settings_get() then to obtain either the loaded
 *          settings or the old ones (if failed), or an empty #GwyDict.
 **/
gboolean
gwy_app_settings_load(const gchar *filename,
                      GError **error)
{
    GwyDict *new_settings;
    GError *err = NULL;
    gchar *buffer = NULL;
    gsize size = 0;

    gwy_debug("Loading settings from `%s'", filename);
    if (!g_file_get_contents(filename, &buffer, &size, &err)) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_FILE,
                    _("Cannot read file contents: %s"), err->message);
        g_clear_error(&err);
        return FALSE;
    }

    if (!size || !buffer) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_EMPTY, _("File is empty."));
        g_free(buffer);
        return FALSE;
    }
#ifdef G_OS_WIN32
    gwy_strkill(buffer, "\r");
#endif
    if (!g_str_has_prefix(buffer, magic_header)) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CORRUPT,
                    _("File is corrupted, magic header does not match."));
        g_free(buffer);
        return FALSE;
    }
    new_settings = gwy_dict_deserialize_from_text(buffer + strlen(magic_header));
    g_free(buffer);
    if (!new_settings) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CORRUPT,
                    _("File is corrupted, deserialization failed."));
        return FALSE;
    }
    gwy_app_settings_free();
    gwy_settings = new_settings;
    gwy_app_settings_set_defaults(gwy_settings);
    gwy_app_settings_apply(gwy_settings);

    //FIXME: deleting "/module/logistic/thetas" stored in wrong
    //FIXME: format during 2.46 development cycle
    gwy_dict_remove_by_prefix(gwy_settings, "/module/logistic/thetas");

    return TRUE;
}

/**
 * gwy_app_settings_merge_string:
 * @s: String contaning a part of Gwyddion settings file.
 * @overwrite: %TRUE to replace existing settings; %FALSE to only set values which do not exist.
 * @error: Location to store loading error to, or %NULL.
 *
 * Loads settings from a string and adds them to the global settings.
 *
 * The contents of string @s should look like a Gwyddion settings file, including the header line "Gwyddion Settings
 * 1.0", even though it is optional.
 *
 * If no settings exist when this function is called, they are initialised like calling gwy_app_settings_get().
 *
 * Returns: Whether it succeeded.
 **/
gboolean
gwy_app_settings_merge_string(const gchar *s,
                              gboolean overwrite,
                              GError **error)
{
    GwyDict *settings, *new_settings;
    const gchar *s_to_parse;
    gchar *buffer = NULL;

    /* Do it first for determinisic behaviour. */
    settings = gwy_app_settings_get();

    if (!s || !*s)
        return TRUE;

    buffer = gwy_strkill(g_strdup(s), "\r");
    s_to_parse = (g_str_has_prefix(buffer, magic_header) ? buffer + strlen(magic_header) : buffer);
    new_settings = gwy_dict_deserialize_from_text(s_to_parse);
    g_free(buffer);
    if (!new_settings) {
        g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CORRUPT,
                    _("File is corrupted, deserialization failed."));
        return FALSE;
    }

    gwy_dict_transfer(new_settings, settings, "/", "/", FALSE, overwrite);
    g_object_unref(new_settings);

    return TRUE;
}

/**
 * gwy_app_settings_create_config_dir:
 * @error: Location to store loading error to, or %NULL.
 *
 * Create gwyddion config directory.
 *
 * Returns: Whether it succeeded (also returns %TRUE if the directory already exists).
 **/
gboolean
gwy_app_settings_create_config_dir(GError **error)
{
    return create_config_dir_real(gwy_get_user_dir(), error);
}

static gboolean
create_config_dir_real(const gchar *cfgdir, GError **error)
{
    gboolean ok = g_file_test(cfgdir, G_FILE_TEST_IS_DIR);
    if (!ok) {
        gwy_debug("Trying to create user config directory %s", cfgdir);
        ok = !g_mkdir(cfgdir, 0700);
        if (!ok) {
            g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CFGDIR,
                        _("Cannot create user config directory %s: %s"), cfgdir, g_strerror(errno));
        }
    }

    if (ok) {
        gchar *ui_dir = g_build_filename(cfgdir, "ui", NULL);
        if (!g_file_test(ui_dir, G_FILE_TEST_IS_DIR)) {
            gwy_debug("Trying to create user ui directory %s", ui_dir);
            ok = !g_mkdir(ui_dir, 0700);
            if (!ok) {
                g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CFGDIR,
                            _("Cannot create user ui directory %s: %s"), ui_dir, g_strerror(errno));
            }
        }
        g_free(ui_dir);
    }

    if (ok) {
        gchar *mod_dir = g_build_filename(cfgdir, "modules", NULL);
        if (!g_file_test(mod_dir, G_FILE_TEST_IS_DIR)) {
            gwy_debug("Trying to create user ui directory %s", mod_dir);
            ok = !g_mkdir(mod_dir, 0700);
            if (!ok) {
                g_set_error(error, GWY_APP_SETTINGS_ERROR, GWY_APP_SETTINGS_ERROR_CFGDIR,
                            _("Cannot create user module directory %s: %s"), mod_dir, g_strerror(errno));
            }
        }
        g_free(mod_dir);
    }

    return ok;
}

static void
gwy_app_settings_set_defaults(GwyDict *settings)
{
    static const gchar default_preferred_gradients[] =
        "BW1\n"
        "Blend2\n"
        "Caribbean\n"
        "Cold\n"
        "DFit\n"
        "Gray\n"
        "Green-Violet\n"
        "Gwyddion.net\n"
        "Lines\n"
        "Olive\n"
        "Rainbow2\n"
        "Red\n"
        "Sky\n"
        "Spectral\n"
        "Spring\n"
        "Warm";
    static const gchar default_preferred_gl_materials[] =
        "Brass\n"
        "Cyan-Plastic\n"
        "OpenGL-Default\n"
        "Emerald\n"
        "Obsidian\n"
        "Pewter\n"
        "Polished-Gold\n"
        "Red-Rubber\n"
        "Silver\n"
        "Warmish-White";

    if (!gwy_dict_contains_by_name(settings, "/mask/alpha"))
        gwy_rgba_store_to_dict(&default_mask_color, settings, "/mask");
    if (!gwy_dict_contains_by_name(settings, "/app/gradients/preferred"))
        gwy_dict_set_string_by_name(settings, "/app/gradients/preferred", default_preferred_gradients);
    if (!gwy_dict_contains_by_name(settings, "/app/glmaterials/preferred"))
        gwy_dict_set_string_by_name(settings, "/app/glmaterials/preferred", default_preferred_gl_materials);
    if (!gwy_dict_contains_by_name(settings, "/app/toolbox/visible/image"))
        gwy_dict_set_boolean_by_name(settings, "/app/toolbox/visible/image", TRUE);
    if (!gwy_dict_contains_by_name(settings, "/app/toolbox/visible/tool"))
        gwy_dict_set_boolean_by_name(settings, "/app/toolbox/visible/tool", TRUE);
}

static void
add_preferred_resource_name(G_GNUC_UNUSED gpointer key,
                            gpointer item,
                            gpointer user_data)
{
    GwyResource *resource = GWY_RESOURCE(item);
    GPtrArray *array = (GPtrArray*)user_data;

    if (gwy_resource_get_preferred(resource))
        g_ptr_array_add(array, (gpointer)gwy_resource_get_name(resource));
}

/**
 * gwy_app_settings_gather:
 * @settings: App settings.
 *
 * Perform settings update that needs to be done only once at shutdown.
 **/
/* TODO: refactor common resource code */
static void
gwy_app_settings_gather(GwyDict *settings)
{
    GPtrArray *preferred;
    const gchar *name;

    /* Preferred resources */
    preferred = g_ptr_array_new();
    gwy_inventory_foreach(gwy_gradients(), &add_preferred_resource_name, preferred);
    g_ptr_array_add(preferred, NULL);
    gwy_dict_pass_string_by_name(settings, "/app/gradients/preferred",
                                     g_strjoinv("\n", (gchar**)preferred->pdata));
    g_ptr_array_set_size(preferred, 0);
    gwy_inventory_foreach(gwy_gl_materials(), &add_preferred_resource_name, preferred);
    g_ptr_array_add(preferred, NULL);
    gwy_dict_pass_string_by_name(settings, "/app/glmaterials/preferred",
                                     g_strjoinv("\n", (gchar**)preferred->pdata));
    g_ptr_array_free(preferred, TRUE);

    /* Default resources */
    name = gwy_inventory_get_default_item_name(gwy_gradients());
    if (name)
        gwy_dict_pass_string_by_name(settings, "/app/gradients/default", g_strdup(name));
    name = gwy_inventory_get_default_item_name(gwy_gl_materials());
    if (name)
        gwy_dict_pass_string_by_name(settings, "/app/glmaterials/default", g_strdup(name));
}

static void
apply_resource_settings(GwyDict *settings,
                        const gchar *prefix, const gchar *name, GwyInventory *inventory)
{
    const gchar *s;
    gchar *key;

    key = g_strconcat(prefix, "/preferred", NULL);
    if (gwy_dict_gis_string_by_name(settings, key, &s)) {
        if (inventory) {
            gchar **preferred = g_strsplit(s, "\n", 0);
            for (gchar **p = preferred; *p; p++) {
                GwyResource *resource;
                if ((resource = gwy_inventory_get_item(inventory, *p)))
                    gwy_resource_set_preferred(resource, TRUE);
            }
            g_strfreev(preferred);
        }
        else
            g_warning("Cannot set preferred %s resources: the Inventory does not exist.", name);
    }
    g_free(key);

    key = g_strconcat(prefix, "/default", NULL);
    if (gwy_dict_gis_string_by_name(settings, key, &s)) {
        if (inventory)
            gwy_inventory_set_default_item_name(inventory, s);
        else
            g_warning("Cannot set the default %s: the Inventory does not exist.", name);
    }
    g_free(key);
}

/**
 * gwy_app_settings_apply:
 * @settings: App settings.
 *
 * Applies initial settings to things that need it.
 **/
static void
gwy_app_settings_apply(GwyDict *settings)
{
    gboolean disabled;

    /* Preferred and default resources. */
    apply_resource_settings(settings, "/app/gradients", "gradient", gwy_gradients());
    apply_resource_settings(settings, "/app/glmaterials", "GL material", gwy_gl_materials());

    /* Globally disabled OpenGL view axes */
    if (gwy_dict_gis_boolean_by_name(settings, "/app/3d/axes/disable", &disabled) && disabled)
        gwy_gl_view_class_disable_axis_drawing(disabled);

    /* Globally disabled logging */
    if (gwy_dict_gis_boolean_by_name(settings, "/app/log/disable", &disabled) && disabled)
        gwy_log_set_enabled(FALSE);
}

/**
 * gwy_app_settings_get_module_dirs:
 *
 * Returns a list of directories to search modules in.
 *
 * Returns: (transfer full):
 *          The list of module directories as a newly allocated array of newly allocated strings, to be freed with
 *          g_strfreev() when not longer needed.
 **/
gchar**
gwy_app_settings_get_module_dirs(void)
{
    gchar **module_dirs = g_new(gchar*, 3);
    module_dirs[0] = gwy_find_self_path("lib", "modules", NULL);
    module_dirs[1] = g_build_filename(gwy_get_user_dir(), "modules", NULL);
    module_dirs[2] = NULL;
    return module_dirs;
}

/**
 * gwy_app_settings_get_settings_filename:
 *
 * Returns a suitable human-readable settings file name.
 *
 * Returns: The file name as a newly allocated string.
 **/
gchar*
gwy_app_settings_get_settings_filename(void)
{
    return g_build_filename(gwy_get_user_dir(), "settings", NULL);
}

/**
 * gwy_app_settings_get_log_filename:
 *
 * Returns a suitable log file name.
 *
 * Returns: The file name as a newly allocated string.
 **/
gchar*
gwy_app_settings_get_log_filename(void)
{
    const gchar *s;

    if ((s = g_getenv("GWYDDION_LOGFILE")))
        return g_strdup(s);

    return g_build_filename(gwy_get_user_dir(), "gwyddion.log", NULL);
}

/**
 * gwy_app_settings_get_recent_file_list_filename:
 *
 * Returns a suitable recent file list file name.
 *
 * Returns: The file name as a newly allocated string.
 **/
gchar*
gwy_app_settings_get_recent_file_list_filename(void)
{
    return g_build_filename(gwy_get_user_dir(), "recent-files", NULL);
}

/**
 * gwy_app_settings_get_default_mask_color:
 * @color: (out): Location where to store the colour.
 *
 * Gets the current default mask colour.
 *
 * Normally, the settings contain the default mask colour. If, for whatever reason, the settings do not exist or the
 * colour it has been deleted, the function fills @color with the initial default mask colour.
 **/
void
gwy_app_settings_get_default_mask_color(GwyRGBA *color)
{
    g_return_if_fail(color);

    if (gwy_settings && gwy_rgba_get_from_dict(color, gwy_settings, "/mask"))
        return;
    *color = default_mask_color;
}

/**
 * gwy_app_settings_error_quark:
 *
 * Returns error domain for application settings operations.
 *
 * See and use %GWY_APP_SETTINGS_ERROR.
 *
 * Returns: The error domain.
 **/
GQuark
gwy_app_settings_error_quark(void)
{
    static GQuark error_domain = 0;

    if (!error_domain)
        error_domain = g_quark_from_static_string("gwy-app-settings-error-quark");

    return error_domain;
}

/**
 * SECTION:settings
 * @title: settings
 * @short_description: Application and module settings
 * @see_also: #GwyParamDef, #GwyParams -- a higher level module settings interface
 *
 * All application and module settings are stored in a one big #GwyDict which can be obtained by
 * gwy_app_settings_get(). Then you can use #GwyDict functions to get and save settings.
 *
 * The rest of the setting manipulating functions is normally useful only in the main application.
 **/

/**
 * GWY_APP_SETTINGS_ERROR:
 *
 * Error domain for application settings operations. Errors in this domain will be from the #GwyAppSettingsError
 * enumeration. See #GError for information on error domains.
 **/

/**
 * GwyAppSettingsError:
 * @GWY_APP_SETTINGS_ERROR_FILE: Settings file is not readable or writable.
 * @GWY_APP_SETTINGS_ERROR_CORRUPT: Settings file contents is corrupt.
 * @GWY_APP_SETTINGS_ERROR_CFGDIR: User configuration directory is not readable or writable or it does not exist and
 *                                 its creation failed.
 * @GWY_APP_SETTINGS_ERROR_EMPTY: Settings file is empty. Unlike corrupt files, which may contain valuable
 *                                information, empty settings files can be overwritten without losing anything.
 *
 * Error codes returned by application settings functions.
 **/

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