/*
 *  $Id: utils.c 29570 2026-03-05 15:49:03Z 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 <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <locale.h>
#include <glib/gprintf.h>
#include <glib/gi18n-lib.h>
#include <gobject/gvaluecollector.h>

#include "libgwyddion/math.h"
#include "libgwyddion/macros.h"
#include "libgwyddion/serializable-boxed.h"
#include "libgwyddion/utils.h"

#include "libgwyddion/internal.h"

/* We do not need anything from this file. But ./configure touches it and we need to recompile after ./configure to
 * get installation paths right. */
#include "libgwyddion/version.h"

#ifndef GWY_LIBDIR
#define GWY_LIBDIR NULL
#endif

#ifndef GWY_DATADIR
#define GWY_DATADIR NULL
#endif

#ifndef GWY_LOCALEDIR
#define GWY_LOCALEDIR NULL
#endif

#ifdef __APPLE__
#define GWYDDION_BUNDLE_ID "net.gwyddion"
#include <CoreFoundation/CoreFoundation.h>
#endif

#ifdef G_OS_WIN32
#include <windows.h>

static HMODULE dll_handle;

BOOL WINAPI
DllMain(HINSTANCE hinstDLL,
        DWORD fdwReason,
        G_GNUC_UNUSED LPVOID lpvReserved)
{
    if (fdwReason == DLL_PROCESS_ATTACH)
        dll_handle = (HMODULE)hinstDLL;
    return TRUE;
}

static gpointer
ensure_win32_topdir(G_GNUC_UNUSED gpointer arg)
{
    return g_win32_get_package_installation_directory_of_module(dll_handle);
}
#endif

G_LOCK_DEFINE_STATIC(mapped_files);
static GHashTable *mapped_files = NULL;       /* Threads: protected by lock */

/**
 * gwy_map_file_contents:
 * @filename: A file to read contents of.
 * @buffer: Buffer to store the file contents.
 * @size: Location to store buffer (file) size.
 * @error: Return location for a #GError.
 *
 * Maps file contents into memory for reading.
 *
 * This is a convenience wrapper for #GMappedFile which looks more like g_file_get_contents(). The mapping is
 * read-only and must be later with gwy_unmap_file_contents(). Unlike g_file_get_contents(), the contents is NOT
 * NUL-terminated. Use @size to find its end.
 *
 * Errors from the #GFileError domain can occur.
 *
 * Returns: Whether it succeeded.  In the case of failure @buffer and @size are reset too.
 **/
gboolean
gwy_map_file_contents(const gchar *filename,
                      const guchar **buffer,
                      gsize *size,
                      GError **error)
{
    g_return_val_if_fail(filename, FALSE);
    g_return_val_if_fail(buffer, FALSE);
    g_return_val_if_fail(size, FALSE);

    GMappedFile *mfile = g_mapped_file_new(filename, FALSE, error);
    if (!mfile) {
        *buffer = NULL;
        *size = 0;
        return FALSE;
    }

    *buffer = g_mapped_file_get_contents(mfile);
    *size = g_mapped_file_get_length(mfile);
    G_LOCK(mapped_files);
    if (!mapped_files)
        mapped_files = g_hash_table_new(g_direct_hash, g_direct_equal);

    gpointer key = (gpointer)*buffer;
    gboolean ok = !g_hash_table_lookup(mapped_files, key);
    g_hash_table_insert(mapped_files, key, mfile);
    G_UNLOCK(mapped_files);

    if (!ok) {
        g_warning("File `%s' was mapped to address %p where we already have mapped a file, which should not happen. "
                  "One of the files will leak.",
                  filename, buffer);
    }

    return TRUE;
}

/**
 * gwy_unmap_file_contents:
 * @buffer: Buffer with file contents as created by gwy_map_file_contents().
 *
 * Ummaps a file mapped to memory for reading.
 *
 * For convenience, it is possible to pass %NULL @buffer. The function is no-op then.
 **/
void
gwy_unmap_file_contents(const guchar *buffer)
{
    if (!buffer)
        return;

    G_LOCK(mapped_files);

    gpointer key = (gpointer)buffer;
    GMappedFile *mfile;
    gboolean ok = FALSE;
    if (mapped_files && (mfile = g_hash_table_lookup(mapped_files, key))) {
        g_mapped_file_unref(mfile);
        g_hash_table_remove(mapped_files, key);
        ok = TRUE;
    }
    G_UNLOCK(mapped_files);

    if (!ok)
        g_warning("No file is known to be mapped to address %p.", buffer);
}

/**
 * gwy_sgettext:
 * @domain: (nullable): Translation domain to use, or %NULL for the domain set with textdomain().
 * @msgid: Message id to translate, possibly with ‘\004’-separated context prefix.
 *
 * Translate a message id containing disambiguating prefix ending with ‘\004’.
 *
 * The control ASCII character 0x04 is used to separate message context in gettext and is used by macros such
 * as C_(). It is not allowed in translatable strings.
 *
 * If the message is translated then the translation is returned. If it is not translated and it contains the
 * character ‘\004’ a pointer into @msgid is returned, to the byte after the separator character.
 *
 * If you can pass both the context and message separately, use the standard macros. Whenever you would need to write
 * ‘\004’ literally or pass a literal string at all, it is probably an incorrect usage. In particular, in direct calls
 * do
 * |[
 * foo(C_("context", "Message"));
 * ]|
 * instead of
 * |[
 * foo(gwy_C("context\004Message"));
 * ]|
 *
 * However, using NC_() and C_() requires duplicating the context and/or passing them around separately. It is
 * sometimes awkward or even not possible to pass the context where it is finally needed.
 *
 * If you need to pass the context and message already in a single string (separated with ‘\004’) you can use this
 * function. It is a simpler alternative to g_dpgettext() which tries to use both ‘|’ and ‘\004’ in a complicated
 * manner when the context is not passed separately. You can use macro gwy_NC() to form source code strings with
 * context. See also macro GWY_C() which expands to gwy_sgettext() with the first argument set to GETTEXT_PACKAGE.
 *
 * The function uses g_dgettext() internally to avoid mixing translated and untranslated messages.
 *
 * This function description is about ten times longer than the implementation. So the entire situation is clearly
 * a mess.
 *
 * Returns: Translated or untranslated message, but in all cases without any context prefix.
 **/
const gchar*
gwy_sgettext(const gchar *domain,
             const gchar *msgid)
{
    const gchar *msgstr = g_dgettext(domain, msgid);
    if (msgstr != msgid)
        return msgstr;

    const gchar *p = strrchr(msgstr, '\004');
    return p ? p+1 : msgstr;
}

#ifdef __APPLE__
static gpointer
ensure_osx_basedir(G_GNUC_UNUSED gpointer arg)
{
    int len, maxlen = 256;
    char *res_url_path;
    gchar *basedir = NULL;

    CFBundleRef bundle_ref = NULL;
    CFStringRef bid_str_ref = NULL;

    bid_str_ref = CFSTR(GWYDDION_BUNDLE_ID);
    bundle_ref = CFBundleGetBundleWithIdentifier(bid_str_ref);
    if (bundle_ref) {
        CFURLRef res_url_ref = NULL, bundle_url_ref = NULL;

        res_url_ref = CFBundleCopyResourcesDirectoryURL(bundle_ref);
        bundle_url_ref = CFBundleCopyBundleURL(bundle_ref);

        if (res_url_ref && bundle_url_ref && !CFEqual(res_url_ref, bundle_url_ref)) {
            res_url_path = malloc(maxlen);

            while (!CFURLGetFileSystemRepresentation(res_url_ref, true, (UInt8*)res_url_path, maxlen)) {
                maxlen *= 2;
                res_url_path = realloc(res_url_path, maxlen);
            }

            len = strlen(res_url_path);
            basedir = g_new(gchar, len + 1);
            strncpy(basedir, res_url_path, len);
            basedir[len] = '\0';
            free(res_url_path);
        }
        if (res_url_ref)
            CFRelease(res_url_ref);
        if (bundle_url_ref)
            CFRelease(bundle_url_ref);
    }

    return basedir;
}

static const gchar*
gwy_osx_find_dir_in_bundle(const gchar *dirname)
{
    static GOnce osx_basedir_once = G_ONCE_INIT;
    return g_once(&osx_basedir_once, ensure_osx_basedir, NULL);
}
#endif

static gboolean
find_self_base_dir(const gchar *dirname, GPtrArray *pathcomps)
{
    static const struct {
        const gchar *id;
        /* Environment variables overriding everthing else, on all operating systems. */
        const gchar *env;
        /* Subdirectory in the filesystem (Unix). */
        const gchar *unixdir;
        /* Subdirectory with respect to the top-level installation directory (Win32). */
        const gchar *win32dir;
        /* Subdirectory to append to the path almost always, except for OS X bundles. There the contents of both
         * ${libdir}/gwyddion and ${datadir}/gwyddion lives directly in Contents/Resources. Locales I have no idea
         * where live, if anywhere. The bundler seems to ignore translations. */
        const gchar *subdir;
    }
    paths[] = {
        { "lib",    "GWYDDION_LIBDIR",    GWY_LIBDIR,    "lib",           "gwyddion3", },
        { "data",   "GWYDDION_DATADIR",   GWY_DATADIR,   "share",         "gwyddion3", },
        { "locale", "GWYDDION_LOCALEDIR", GWY_LOCALEDIR, "share\\locale", NULL,        },
    };
    const gchar *base = NULL;
    guint i;

    /* 1. Environment variables allow the user to override everything. */
    for (i = 0; i < G_N_ELEMENTS(paths); i++) {
        if (gwy_strequal(dirname, paths[i].id) && (base = g_getenv(paths[i].env)))
            break;
    }

    /* 2. Apple bundle has the highest priority on Apple. */
#ifdef __APPLE__
    if (!base && (base = gwy_osx_find_dir_in_bundle(dirname))) {
        g_ptr_array_add(pathcomps, base);
        return TRUE;
    }
#endif

    /* 3. Look for compile-time defined paths on Unix. */
#ifdef G_OS_UNIX
    if (!base) {
        for (i = 0; i < G_N_ELEMENTS(paths); i++) {
            if (gwy_strequal(dirname, paths[i].id) && (base = paths[i].unixdir))
                break;
        }
    }
#endif

    /* 4. Look for installation directory on Win32. */
#ifdef G_OS_WIN32
    static GOnce topdir_once = G_ONCE_INIT;
    const gchar *topdir = (const gchar*)g_once(&topdir_once, ensure_win32_topdir, NULL);
    if (!base) {
        for (i = 0; i < G_N_ELEMENTS(paths); i++) {
            if (gwy_strequal(dirname, paths[i].id)) {
                base = paths[i].win32dir;
                break;
            }
        }
    }
#endif

    if (!base) {
        g_critical("Cannot find directory for `%s'", dirname);
        return FALSE;
    }

    gwy_debug("for <%s> base = <%s>, dir = <%s>", dirname, base, paths[i].subdir);
    g_ptr_array_add(pathcomps, (gpointer)base);
    if (paths[i].subdir)
        g_ptr_array_add(pathcomps, (gpointer)paths[i].subdir);
    return TRUE;
}

/**
 * gwy_find_self_path:
 * @dirname: A gwyddion base directory name:
 *           <literal>"lib"</literal>, <literal>"locale"</literal>, or <literal>"data"</literal>.
 * @...: Additional path components, %NULL-terminated.
 *
 * Finds a system Gwyddion directory.
 *
 * On all operating system, the directories can be overriden with environment variables which have the highest
 * priority (see gwyddion manual page). Otherwise…
 *
 * In an OS X bundle, a location in the bundle is returned.
 *
 * On Unix, a compiled-in path is returned.
 *
 * On Win32, the directory where the libgwyddion DLL from which this function was called resides is taken as the base
 * and the location of other Gwyddion directories is calculated from it.
 *
 * The returned value is not actually tested for existence. It is up to the caller.
 *
 * Icons, modules and system resources like gradients reside in subdirectories. Use the additional path components to
 * construct their path directly. If you are looking for a single file you can use them to construct path to this
 * specific file, although usually one looks for directories.
 *
 * To obtain the Gwyddion user directory see gwy_get_user_dir().
 *
 * Returns: The path as a newly allocated string.
 **/
gchar*
gwy_find_self_path(const gchar *dirname,
                   ...)
{
    g_return_val_if_fail(dirname, NULL);

    GPtrArray *pathcomp = g_ptr_array_new();
    if (!find_self_base_dir(dirname, pathcomp)) {
        g_ptr_array_free(pathcomp, TRUE);
        return NULL;
    }

    va_list ap;
    va_start(ap, dirname);
    for (const gchar *name = va_arg(ap, gchar*); name; name = va_arg(ap, gchar*))
        g_ptr_array_add(pathcomp, (gpointer)name);
    va_end(ap);

    g_ptr_array_add(pathcomp, NULL);
    gchar *retval = g_build_filenamev((gchar**)pathcomp->pdata);
    g_ptr_array_free(pathcomp, TRUE);

    return retval;
}

static gpointer
ensure_userdir(G_GNUC_UNUSED gpointer arg)
{
    /* This is not officially documented. We need a global override to run some tests which would write files in
     * user's config/data/whatever directory. */
    const gchar *env_user_dir = g_getenv("GWYDDION_HOMEDIR");
    if (env_user_dir && *env_user_dir)
        return g_strdup(env_user_dir);

    const gchar *gwydir =
#ifdef G_OS_WIN32
        "gwyddion3";
#else
        ".gwyddion3";
#endif

    return g_build_filename(gwy_get_home_dir(), gwydir, NULL);
}

/**
 * gwy_get_user_dir:
 *
 * Returns the directory where Gwyddion user settings and data should be stored.
 *
 * On Unix this is usually a dot-directory in user's home directory.  On modern Win32 the returned directory resides
 * in user's Documents and Settings. On silly platforms or silly occasions, silly locations (namely a temporary
 * directory) can be returned as fallback.
 *
 * To obtain a Gwyddion system directory see gwy_find_self_dir().
 *
 * Returns: The directory as a constant string that should not be freed.
 **/
const gchar*
gwy_get_user_dir(void)
{
    static GOnce userdir_once = G_ONCE_INIT;
    return (const gchar*)g_once(&userdir_once, ensure_userdir, NULL);
}

static gpointer
ensure_homedir(G_GNUC_UNUSED gpointer arg)
{
    const gchar *homedir;

    homedir = g_get_home_dir();
    if (!homedir || !*homedir)
        homedir = g_get_tmp_dir();
#ifdef G_OS_WIN32
    if (!homedir || !*homedir)
        homedir = "C:\\Windows";  /* XXX :-))) */
#else
    if (!homedir || !*homedir)
        homedir = "/tmp";
#endif

    return (gpointer)homedir;
}

/**
 * gwy_get_home_dir:
 *
 * Returns home directory, or temporary directory as a fallback.
 *
 * Under normal circumstances the same string as g_get_home_dir() would return is returned.  But on MS Windows,
 * something like "C:\Windows\Temp" can be returned too, as it is as good as anything else (we can write there).
 *
 * Returns: Something usable as user home directory.  It may be silly, but never %NULL or empty.
 **/
const gchar*
gwy_get_home_dir(void)
{
    static GOnce homedir_once = G_ONCE_INIT;
    return (const gchar*)g_once(&homedir_once, ensure_homedir, NULL);
}

/**
 * gwy_canonicalize_path:
 * @path: A filesystem path.
 *
 * Canonicalizes a filesystem path.
 *
 * Particularly it makes the path absolute, resolves `..' and `.', and fixes slash sequences to single slashes.  On
 * Win32 it also converts all backslashes to slashes along the way.
 *
 * Note this function does NOT resolve symlinks, use g_file_read_link() for that.
 *
 * Returns: The canonical path, as a newly created string.
 **/
gchar*
gwy_canonicalize_path(const gchar *path)
{
    gchar *spath, *p0, *p, *last_slash;
    gsize i;

    g_return_val_if_fail(path, NULL);

    /* absolutize */
    if (!g_path_is_absolute(path)) {
        p = g_get_current_dir();
        spath = g_build_filename(p, path, NULL);
        g_free(p);
    }
    else
        spath = g_strdup(path);
    p = spath;

#ifdef G_OS_WIN32
    /* convert backslashes to slashes */
    while (*p) {
        if (*p == '\\')
            *p = '/';
        p++;
    }
    p = spath;

    /* skip c:, //server */
    if (g_ascii_isalpha(*p) && p[1] == ':')
        p += 2;
    else if (*p == '/' && p[1] == '/') {
        p = strchr(p+2, '/');
        /* silly, but better this than a coredump... */
        if (!p)
            return spath;
    }
    /* now p starts with the `root' / on all systems */
#endif
    g_return_val_if_fail(*p == '/', spath);

    p0 = p;
    while (*p) {
        if (*p == '/') {
            if (p[1] == '.') {
                if (p[2] == '/' || !p[2]) {
                    /* remove from p here */
                    for (i = 0; p[i+2]; i++)
                        p[i] = p[i+2];
                    p[i] = '\0';
                }
                else if (p[2] == '.' && (p[3] == '/' || !p[3])) {
                    /* remove from last_slash here */
                    /* ignore if root element */
                    if (p == p0) {
                        for (i = 0; p[i+3]; i++)
                            p[i] = p[i+3];
                        p[i] = '\0';
                    }
                    else {
                        for (last_slash = p-1; *last_slash != '/'; last_slash--)
                          ;
                        for (i = 0; p[i+3]; i++)
                            last_slash[i] = p[i+3];
                        last_slash[i] = '\0';
                        p = last_slash;
                    }
                }
                else
                    p++;
            }
            else {
                /* remove a continouos sequence of slashes */
                for (last_slash = p; *last_slash == '/'; last_slash++)
                    ;
                last_slash--;
                if (last_slash > p) {
                    for (i = 0; last_slash[i]; i++)
                        p[i] = last_slash[i];
                    p[i] = '\0';
                }
                else
                    p++;
            }
        }
        else
            p++;
    }
    /* a final `..' could fool us into discarding the starting slash */
    if (!*p0) {
      *p0 = '/';
      p0[1] = '\0';
    }

    return spath;
}

/**
 * gwy_filename_ignore:
 * @filename_sys: File name in GLib encoding.
 *
 * Checks whether file should be ignored.
 *
 * This function checks for common file names indicating files that should be normally ignored.  Currently it means
 * backup files (ending with ~ or .bak) and Unix hidden files (starting with a dot).
 *
 * Returns: %TRUE to ignore this file, %FALSE otherwise.
 **/
gboolean
gwy_filename_ignore(const gchar *filename_sys)
{
    if (!filename_sys
        || !*filename_sys
        || filename_sys[0] == '.'
        || g_str_has_suffix(filename_sys, "~")
        || g_str_has_suffix(filename_sys, ".bak")
        || g_str_has_suffix(filename_sys, ".BAK"))
        return TRUE;

    return FALSE;
}

/**
 * gwy_object_set_or_reset:
 * @object: A #GObject.
 * @type: The type whose properties are to reset, may be zero for all types. The object must be of this type (more
 *        precisely @object is-a @type must hold).
 * @...: %NULL-terminated list of name/value pairs of properties to set as in g_object_set().  It can be %NULL alone
 *       to only reset properties to defaults.
 *
 * Sets object properties, resetting other properties to defaults.
 *
 * All explicitly specified properties are set.  In addition, all unspecified settable properties of type @type (or
 * all unspecified properties if @type is 0) are reset to defaults.  Settable means the property is writable and not
 * construction-only.
 *
 * The order in which properties are set is undefined beside keeping the relative order of explicitly specified
 * properties, therefore this function is not generally usable for objects with interdependent properties.
 *
 * Unlike g_object_set(), it does not set properties that already have the requested value, as a consequences
 * notifications are emitted only for properties which actually change.
 **/
void
gwy_object_set_or_reset(gpointer object,
                        GType type,
                        ...)
{
    GValue value, cur_value, new_value;
    GObjectClass *klass;
    GParamSpec **pspec;
    gboolean *already_set;
    const gchar *name;
    va_list ap;
    gint nspec, i;

    klass = G_OBJECT_GET_CLASS(object);
    g_return_if_fail(G_IS_OBJECT_CLASS(klass));
    g_return_if_fail(!type || G_TYPE_CHECK_INSTANCE_TYPE(object, type));

    g_object_freeze_notify(object);

    pspec = g_object_class_list_properties(klass, &nspec);
    already_set = g_new(gboolean, nspec);
    gwy_clear(already_set, nspec);

    gwy_clear1(cur_value);
    gwy_clear1(new_value);

    va_start(ap, type);
    for (name = va_arg(ap, gchar*); name; name = va_arg(ap, gchar*)) {
        gchar *error = NULL;

        for (i = 0; i < nspec; i++) {
            if (gwy_strequal(pspec[i]->name, name))
                break;
        }

        /* Do only minimal checking, because we cannot avoid re-checking in g_object_set_property(), or at least not
         * w/o copying a large excerpt of GObject here.  Considerable amount of work is still performed inside GObject
         * again... */
        if (i == nspec) {
            g_warning("object class `%s' has no property named `%s'", G_OBJECT_TYPE_NAME(object), name);
            break;
        }

        G_VALUE_COLLECT_INIT(&new_value, pspec[i]->value_type, ap, 0, &error);
        if (error) {
            g_warning("%s", error);
            g_free(error);
            g_value_unset(&new_value);
            break;
        }

        g_value_init(&cur_value, pspec[i]->value_type);
        g_object_get_property(object, name, &cur_value);
        if (g_param_values_cmp(pspec[i], &new_value, &cur_value) != 0)
            g_object_set_property(object, name, &new_value);

        g_value_unset(&cur_value);
        g_value_unset(&new_value);
        already_set[i] = TRUE;
    }
    va_end(ap);

    gwy_clear1(value);
    for (i = 0; i < nspec; i++) {
        if (already_set[i] || !(pspec[i]->flags & G_PARAM_WRITABLE) || (pspec[i]->flags & G_PARAM_CONSTRUCT_ONLY)
            || (type && pspec[i]->owner_type != type))
            continue;

        g_value_init(&value, pspec[i]->value_type);
        g_object_get_property(object, pspec[i]->name, &value);
        if (!g_param_value_defaults(pspec[i], &value)) {
            g_param_value_set_default(pspec[i], &value);
            g_object_set_property(object, pspec[i]->name, &value);
        }

        g_value_unset(&value);
    }

    g_free(pspec);
    g_free(already_set);

    g_object_thaw_notify(object);
}

/**
 * gwy_set_member_object:
 * @instance: An object instance.
 * @member_object: Another object to be owned by @instanced, or %NULL.
 * @expected_type: The type of @member_object.  It is checked and a critical message is emitted if it does not
 *                 conform.
 * @member_field: Pointer to location storing the current member object to be replaced by @member_object.
 * @...: List of quadruplets of the form signal name, #GCallback callback, #gulong pointer to location to hold the
 *       signal handler id, and #GConnectFlags connection flags.
 *
 * Replaces a member object of another object, handling signal connection and disconnection.
 *
 * If @member_object is not %NULL a reference is taken, sinking any floating objects (and conversely, the reference to
 * the previous member object is released).
 *
 * The purpose is to simplify bookkeeping in classes that have settable member objects and (usually but not
 * necessarily) need to connect to some signals of these member objects.  Since this function both connects and
 * disconnects signals it must be always called with the same set of signals, including callbacks and flags, for
 * a specific member object.
 *
 * Example for a <type>GwyFoo</type> class owning a #GwyGradient member object, assuming the usual conventions:
 * |[
 * typedef struct _GwyFooPrivate GwyFooPrivate;
 *
 * struct _GwyFooPrivate {
 *     GwyGradient *gradient;
 *     gulong gradient_data_changed_id;
 * };
 *
 * static gboolean
 * set_gradient(GwyFoo *foo)
 * {
 *     GwyFooPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE(foo, GWY_TYPE_FOO,
 *                                                       GwyFooPrivate);
 *     if (!gwy_set_member_object(foo, gradient, GWY_TYPE_GRADIENT,
 *                                &priv->gradient,
 *                                "data-changed", &foo_gradient_data_changed,
 *                                &priv->gradient_data_changed_id,
 *                                G_CONNECT_SWAPPED,
 *                                NULL))
 *         return FALSE;
 *
 *     // Do whatever else needs to be done if the gradient changes.
 *     return TRUE;
 * }
 * ]|
 * The gradient setter then usually only calls <function>set_gradient()</function> and disposing of the member object
 * again only calls <function>set_gradient()</function> but with %NULL gradient.
 *
 * Returns: %TRUE if @member_field was changed.  %FALSE means the new member is identical to the current one and the
 *          function reduced to no-op (or that an assertion faled).
 **/
gboolean
gwy_set_member_object(gpointer instance,
                      gpointer member_object,
                      GType expected_type,
                      gpointer member_field,
                      ...)
{
    gpointer *pmember = (gpointer*)member_field;
    gpointer old_member = *pmember;
    const gchar *signal_name;

    if (old_member == member_object)
        return FALSE;

    g_return_val_if_fail(!member_object || G_TYPE_CHECK_INSTANCE_TYPE(member_object, expected_type), FALSE);

    if (member_object)
        g_object_ref_sink(member_object);

    if (old_member) {
        va_list ap;
        va_start(ap, member_field);
        for (signal_name = va_arg(ap, const gchar*); signal_name; signal_name = va_arg(ap, const gchar*)) {
            G_GNUC_UNUSED GCallback handler = va_arg(ap, GCallback);
            gulong *handler_id = va_arg(ap, gulong*);
            G_GNUC_UNUSED GConnectFlags flags = va_arg(ap, GConnectFlags);
            if (*handler_id) {
                g_signal_handler_disconnect(old_member, *handler_id);
                *handler_id = 0;
            }
        }
        va_end(ap);
    }

    *pmember = member_object;

    if (member_object) {
        va_list ap;
        va_start(ap, member_field);
        for (signal_name = va_arg(ap, const gchar*); signal_name; signal_name = va_arg(ap, const gchar*)) {
            GCallback handler = va_arg(ap, GCallback);
            gulong *handler_id = va_arg(ap, gulong*);
            GConnectFlags flags = va_arg(ap, GConnectFlags);
            *handler_id = g_signal_connect_data(member_object, signal_name, handler, instance, NULL, flags);
        }
        va_end(ap);
    }

    if (old_member)
        g_object_unref(old_member);

    return TRUE;
}

/**
 * gwy_assign_boxed:
 * @target: Pointer to target boxed, typically a struct field.
 * @newvalue: (nullable): New value of the boxed, may be %NULL.
 * @boxed_type: Type of the boxed value.  It must be a registered serialisable type (see
 *              gwy_serializable_boxed_register_static()).
 *
 * Assigns a serialisable boxed value, checking for equality and handling %NULL<!-- -->s.
 *
 * This function simplifies handling of serialisable boxed value setters, using gwy_serializable_boxed_equal() and
 * gwy_serializable_boxed_assign() for correct comparison and value assignment.
 *
 * Any of the old and new value can be %NULL.  If both values are equal (including both unset), the function returns
 * %FALSE.
 *
 * Returns: %TRUE if the target boxed has changed.
 **/
gboolean
gwy_assign_boxed(gpointer *target,
                 gconstpointer newvalue,
                 GType boxed_type)
{
    if (*target && newvalue) {
        if (!gwy_serializable_boxed_equal(boxed_type, *target, newvalue)) {
            gwy_serializable_boxed_assign(boxed_type, *target, newvalue);
            return TRUE;
        }
    }
    else if (*target) {
        g_boxed_free(boxed_type, *target);
        *target = NULL;
        return TRUE;
    }
    else if (newvalue) {
        *target = g_boxed_copy(boxed_type, newvalue);
        return TRUE;
    }
    return FALSE;
}

/* Transfer units by value (gwy_foo_copy_units()). */
void
_gwy_copy_unit(GwyUnit *source, GwyUnit **dest)
{
    if (source == *dest)
        return;

    if (!source || gwy_unit_equal_string(source, NULL)) {
        /* Cannot do g_clear_object(dest) if @dest exists because that could make unit members change over time (by
         * destroying them and creating different objects later), which we worked hard to avoid. */
        if (*dest)
            gwy_unit_clear(*dest);
        return;
    }

    if (*dest)
        gwy_unit_assign(*dest, source);
    else
        *dest = gwy_unit_copy(source);
}

/**
 * gwy_get_decimal_separator:
 *
 * Find the decimal separator for the current locale.
 *
 * This is a localeconv() wrapper.
 *
 * Returns: The decimal separator for the current locale as a static string.
 **/
const gchar*
gwy_get_decimal_separator(void)
{
    struct lconv *locale_data;
    const gchar *sep;

    /* XXX: The localeconv() man page says that the printf() family of functions may not honour the current locale.
     * But we kind of count on them doing so -- and hopefully the g-prefixed do.
     *
     * Also, localeconv() seems not working in Android? We probably don't care. */
    locale_data = localeconv();
    sep = locale_data->decimal_point;
    if (!sep || !strlen(sep)) {
        g_warning("Cannot get decimal dot information from localeconv().");
        return ".";
    }
    return g_intern_string(sep);
}

/**
 * gwy_append_doubles_to_gstring:
 * @str: String to append the formated numbers to.
 * @values: (array length=n): Array of double values to format.
 * @n: Number of values in @values to format.
 * @precision: Format precision, within the standard precision of double.
 * @field_separator: String to put between each two formatted numbers, usually a space or tab character.
 * @force_decimal_dot: %TRUE to ensure the numbers use a decimal dot, regardless of locale. %FALSE to honour the
 *                     current locale.
 *
 * Formats a sequence of double values to a #GString.
 **/
void
gwy_append_doubles_to_gstring(GString *str,
                              const gdouble *values,
                              guint n,
                              gint precision,
                              const gchar *field_separator,
                              gboolean force_decimal_dot)
{
    gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
    gboolean must_fix_decimal_sep;
    const gchar *decimal_sep;
    guint i, seplen;
    gchar *pos;

    /* Do the analysis once. */
    decimal_sep = gwy_get_decimal_separator();
    seplen = strlen(decimal_sep);
    must_fix_decimal_sep = (force_decimal_dot && !gwy_strequal(decimal_sep, "."));

    /* Format a bunch of numbers. */
    for (i = 0; i < n; i++) {
        g_snprintf(buf, sizeof(buf), "%.*g", precision, values[i]);
        if (must_fix_decimal_sep && (pos = strstr(buf, decimal_sep))) {
            pos[0] = '.';
            if (seplen == 1)
                g_string_append(str, buf);
            else {
                pos[1] = '\0';
                g_string_append(str, buf);
                g_string_append(str, pos + seplen);
            }
        }
        else
            g_string_append(str, buf);

        g_string_append(str, field_separator);
    }
    if (n)
        g_string_truncate(str, str->len - strlen(field_separator));
}

/**
 * gwy_parse_doubles:
 * @s: String with floating point data to parse.
 * @values: Pre-allocated array where to store values (for a fixed number of values).
 * @flags: Parsing flags.
 * @nlines: Location where to store the number of lines. Initialise to the number of lines for a fixed number of
 *          lines or to -1 for unknown. The value is only changed if initially -1.
 * @ncols: Location where to store the number of columns. Initialise to the number of columns for a fixed number of
 *         columns or to -1 for unknown. The value is only changed if initially -1.
 * @endptr: (nullable): Location where to store pointer after the last parsed character in @s. Possibly %NULL.
 * @error: Return location for a #GError.
 *
 * Parse a block of text floating point values.
 *
 * Any combinations of a priori known and unknown dimensions is possible. For an unknown number of columns the number
 * of columns must still be the same on each line, unless the flag %GWY_PARSE_DOUBLES_FREE_FORM is passed. If any
 * dimension is unknown, @values cannot be preallocated as must be %NULL.
 *
 * Currently the values must be whitespace separated and in the POSIX format.
 *
 * If the flag %GWY_PARSE_DOUBLES_EMPTY is passed and the string contains not data, it is not considered an error
 * and @error is not set. However, the function returns %NULL in such case. If you allow empty data you need to check
 * the error to distinguish them from failure.
 *
 * Returns: On success, either @values (if non-%NULL @values was passed) or a newly allocated array with the data.
 *          On failure, %NULL is returned.
 **/
gdouble*
gwy_parse_doubles(const gchar *s,
                  gdouble *values,
                  GwyParseDoublesFlags flags,
                  gint *nlines,
                  gint *ncols,
                  gchar **endptr,
                  GError **error)
{
    GArray *storage = NULL;
    gint nreadcols, nreadlines, nreadn = -1, line, col, n;
    gboolean free_values_on_fail = FALSE, is_newline, is_free_form = (flags & GWY_PARSE_DOUBLES_FREE_FORM);
    gchar *end;
    gdouble v;

    g_return_val_if_fail(s, NULL);
    g_return_val_if_fail(nlines, NULL);
    g_return_val_if_fail(ncols, NULL);

    if (values) {
        /* Reading an unknown number of values into fixed array = buffer overflow. Refuse. */
        g_return_val_if_fail(*nlines >= 0, NULL);
        g_return_val_if_fail(*ncols >= 0, NULL);
    }
    if (!*nlines || !*ncols)
        return NULL;

    nreadlines = *nlines;
    nreadcols = *ncols;
    if (nreadlines > 0 && nreadcols > 0)
        nreadn = nreadlines*nreadcols;
    if (!values) {
        free_values_on_fail = TRUE;
        if (nreadn > 0)
            values = g_new(gdouble, nreadn);
        else
            storage = g_array_new(FALSE, FALSE, sizeof(gdouble));
    }
    g_return_val_if_fail(storage || values, NULL);

    line = col = n = 0;
    while (line < nreadlines || nreadlines < 0 || is_free_form) {
        v = g_ascii_strtod(s, &end);
        if (end == s) {
            /* We finished parsing the string or garbage at the end is allowed. */
            if (!*end || g_ascii_isspace(*end) || !(flags & GWY_PARSE_DOUBLES_COMPLETELY)) {
                if (nreadlines < 0) {
                    /* One long line. */
                    if (nreadcols < 0) {
                        *ncols = n;
                        *nlines = 1;
                        goto finished;
                    }
                    /* A full rectangular block, still possibly empty. */
                    if (col == 0) {
                        *ncols = (nreadcols > 0 ? nreadcols : 0);
                        *nlines = line;
                        goto finished;
                    }
                    if (is_free_form) {
                        *ncols = 1;
                        *nlines = n;
                        goto finished;
                    }
                }
                /* We are also done here if we are asked to read one line with some values. */
                if (nreadlines == 1 && nreadcols < 0) {
                    *ncols = n;
                    *nlines = 1;
                    goto finished;
                }
                g_set_error(error, GWY_PARSE_DOUBLES_ERROR, GWY_PARSE_DOUBLES_TRUNCATED,
                            _("Data ended prematurely at line %d and column %d."), line+1, col+1);
                goto fail;
            }
            else {
                g_set_error(error, GWY_PARSE_DOUBLES_ERROR, GWY_PARSE_DOUBLES_MALFORMED,
                            _("Malformed data encountered at line %d and column %d."), line+1, col+1);
                goto fail;
            }
        }
        col++;
        is_newline = FALSE;
        /* This automatically skips any number of empty lines. */
        for (s = end; g_ascii_isspace(*s); s++) {
            if (*s == '\n' || *s == '\r')
                is_newline = TRUE;
        }

        if (is_newline) {
            line++;
            if (nreadcols < 0)
                nreadcols = col;
            else if (!is_free_form && nreadcols != col) {
                g_set_error(error, GWY_PARSE_DOUBLES_ERROR, GWY_PARSE_DOUBLES_NONUNIFORM,
                            _("Line %d has %d columns instead of expected %d."), line, col, nreadcols);
                goto fail;
            }
            col = 0;
        }

        if (storage)
            g_array_append_val(storage, v);
        else
            values[n] = v;

        /* With free form we do not really care about line breaks. And we definitely do not want to update @nlines
         * and @ncols to whatever random stuff we found. */
        if (++n == nreadn && is_free_form)
            goto finished;
    }

    /* We can only get here after parsing an explicitly given number of lines. So we must have successfully read at
     * least one full line. */
    g_assert(line == *nlines);
    g_assert(nreadcols > 0);
    if (*ncols > 0) {
        g_assert(nreadcols == *ncols);
    }
    else
        *ncols = nreadcols;

finished:
    if (endptr)
        *endptr = end;
    if (storage)
        values = (gdouble*)g_array_free(storage, FALSE);
    if (n || (flags & GWY_PARSE_DOUBLES_EMPTY_OK))
        return values;

    g_set_error(error, GWY_PARSE_DOUBLES_ERROR, GWY_PARSE_DOUBLES_EMPTY, _("Empty data."));

fail:
    if (endptr)
        *endptr = end;
    if (free_values_on_fail)
        g_free(values);
    return NULL;
}

static gpointer
ensure_error_domain(G_GNUC_UNUSED gpointer arg)
{
    return GUINT_TO_POINTER(g_quark_from_static_string("gwy-parse-doubles-error-quark"));
}

/**
 * gwy_parse_doubles_error_quark:
 *
 * Returns error domain for floating point value parsing and evaluation.
 *
 * See and use %GWY_PARSE_DOUBLES_ERROR.
 *
 * Returns: The error domain.
 **/
GQuark
gwy_parse_doubles_error_quark(void)
{
    static GOnce error_once = G_ONCE_INIT;
    return GPOINTER_TO_UINT(g_once(&error_once, ensure_error_domain, NULL));
}

/**
 * gwy_fopen:
 * @filename: a pathname in the GLib file name encoding (UTF-8 on Windows)
 * @mode: a string describing the mode in which the file should be opened
 *
 * A wrapper for the stdio fopen() function. The fopen() function opens a file and associates a new stream with it.
 *
 * Because file descriptors are specific to the C library on Windows, and a file descriptor is part of the FILE
 * struct, the FILE* returned by this function makes sense only to functions in the same C library. Thus if the
 * GLib-using code uses a different C library than GLib does, the FILE* returned by this function cannot be passed to
 * C library functions like fprintf() or fread().
 *
 * See your C library manual for more details about fopen().
 *
 * Returns: A FILE* if the file was successfully opened, or %NULL if an error occurred.
 **/
#ifdef G_OS_WIN32
FILE*
gwy_fopen(const char *filename, const char *mode)
{
    wchar_t *wfilename = g_utf8_to_utf16(filename, -1, NULL, NULL, NULL);
    wchar_t *wmode;
    FILE *stream;
    int save_errno;

    if (!wfilename) {
        errno = EINVAL;
        return NULL;
    }

    wmode = g_utf8_to_utf16(mode, -1, NULL, NULL, NULL);

    if (!wmode) {
        g_free(wfilename);
        errno = EINVAL;
        return NULL;
    }

    /* See "Security Enhancements in the CRT".  But it breaks in WinXP and that we would rather avoid.
    save_errno = _wfopen_s(&stream, wfilename, wmode);
    */
    stream = _wfopen(wfilename, wmode);
    save_errno = errno;

    g_free(wfilename);
    g_free(wmode);

    errno = save_errno;
    return stream;
}

/**
 * gwy_fprintf:
 * @file: the stream to write to.
 * @format: a standard printf() format string, but notice
 *          <link linkend='glib-String-Utility-Functions'>string precision
 *          pitfalls</link>
 * @...: the arguments to insert in the output.
 *
 * An implementation of the standard fprintf() function which supports positional parameters, as specified in the
 * Single Unix Specification.
 *
 * Returns: the number of bytes printed.
 **/
int
gwy_fprintf(FILE        *file,
            char const *format,
            ...)
{
    gchar *string = NULL;
    va_list args;
    gint retval;

    va_start(args, format);
    retval = g_vasprintf(&string, format, args);
    va_end(args);
    fputs(string, file);
    g_free(string);

    return retval;
}
#else
#undef gwy_fopen
#undef gwy_fprintf

FILE*
gwy_fopen(const char *filename,
          const char *mode)
{
    return fopen(filename, mode);
}

int
gwy_fprintf(FILE        *file,
            char const *format,
            ...)
{
    va_list args;
    int retval;

    va_start(args, format);
    retval = vfprintf(file, format, args);
    va_end(args);

    return retval;
}
#endif

/**
 * SECTION: utils
 * @title: Utilities
 * @short_description: Miscellaneous utility functions
 * @see_also: <link linkend="libgwyddion-gwymacros">gwymacros</link> -- utility macros
 *
 * Various utility functions: creating GLib lists from hash tables gwy_hash_table_to_list_cb()), protably finding
 * Gwyddion application directories (gwy_find_self_dir()), string functions (gwy_strreplace()), path manipulation
 * (gwy_canonicalize_path()).
 **/

/**
 * GWY_PARSE_DOUBLE_ERROR:
 *
 * Error domain for floating point data parsing and evaluation. Errors in this domain will be from the
 * #GwyParseDoublesError enumeration. See #GError for information on error domains.
 **/

/**
 * GwyParseDoublesFlags:
 * @GWY_PARSE_DOUBLES_EMPTY_OK: Do not set error (and return %NULL) when there are no data.
 * @GWY_PARSE_DOUBLES_COMPLETELY: For unknown number of values, consider an error when they are followed by
 *                                non-numerical garbage. This flag has no effect for a known number of values.
 * @GWY_PARSE_DOUBLES_FREE_FORM: Do not distinguish line breaks from other whitespace; just read the requested number
 *                               of values. For an unknown number of values they will be reported as single-column.
 *
 * Type of flags passed to gwy_parse_doubles().
 **/

/**
 * GWY_PARSE_DOUBLES_ERROR:
 *
 * Error domain for parsing floating point data.
 **/

/**
 * GwyParseDoublesError:
 * @GWY_PARSE_DOUBLES_EMPTY: There are no data at all.
 * @GWY_PARSE_DOUBLES_TRUNCATED: There are fewer values than expected.
 * @GWY_PARSE_DOUBLES_MALFORMED: Non-numerical garbage encounted before reading the expected number of values.
 * @GWY_PARSE_DOUBLES_NONUNIFORM: Lines do not have the expected constant number of columns.
 *
 * Type of error which can occur in gwy_parse_doubles().
 **/

/**
 * GwySetFractionFunc:
 * @fraction: Progress estimate as a number from the interval [0,1].
 *
 * Type of function for reporting progress of a long computation.
 *
 * Usually you want to use gwy_app_wait_set_fraction().
 *
 * Returns: %TRUE if the computation should continue; %FALSE if it should be cancelled.
 **/

/**
 * GwySetMessageFunc:
 * @message: Message to be shown together with the progress fraction.  If the computation has stages the messages
 *           should reflect this. Otherwise at least some general message should be set.
 *
 * Type of function for reporting what a long computation is doing now.
 *
 * Usually you want to use gwy_app_wait_set_message().
 *
 * Returns: %TRUE if the computation should continue; %FALSE if it should be cancelled.
 **/

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