/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */

/* eel-preferences.c - Preference peek/poke/notify implementation.

   Copyright (C) 1999, 2000 Eazel, Inc.

   The Mate Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Mate Library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Mate Library; see the file COPYING.LIB.  If not,
   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.

   Authors: Ramiro Estrugo <ramiro@eazel.com>
*/

#include <config.h>
#include "eel-preferences.h"

#include "eel-debug.h"
#include "eel-mateconf-extensions.h"
#include "eel-lib-self-check-functions.h"
#include "eel-enumeration.h"
#include "eel-glib-extensions.h"
#include "eel-string.h"
#include <mateconf/mateconf-client.h>
#include <mateconf/mateconf.h>
#include <gtk/gtk.h>

/* An enumeration used for updating auto-storage variables in a type-specific way.
 * FIXME: there is another enumeration like this in eel-global-preferences.c,
 * used for different purposes but in a related way. Should we combine them?
 */
typedef enum
{
    PREFERENCE_BOOLEAN = 1,
    PREFERENCE_INTEGER,
    PREFERENCE_STRING,
    PREFERENCE_STRING_ARRAY,
    PREFERENCE_STRING_ARRAY_AS_QUARKS
} PreferenceType;

/*
 * PreferencesEntry:
 *
 * A structure to manage preference hash table nodes.
 * Preferences are hash tables.  The hash key is the preference name
 * (a string).  The  hash value is a pointer of the following struct:
 */
typedef struct
{
    char *name;
    char *description;
    PreferenceType type;
    gboolean invisible;
    GList *callback_list;
    GList *auto_storage_list;
    int mateconf_connection_id;
    char *enumeration_id;
    MateConfValue *fallback;
} PreferencesEntry;

/*
 * PreferencesCallbackEntry:
 *
 * A structure to manage callback lists.  A callback list is a GList.
 * The callback_data in each list node is a pointer to the following
 * struct:
 */
typedef struct
{
    EelPreferencesCallback callback;
    gpointer callback_data;
} PreferencesCallbackEntry;

static GHashTable *global_table = NULL;
static char *storage_path = NULL;
static gboolean initialized = FALSE;

static void              preferences_global_table_free             (void);
static char *            preferences_key_make                      (const char               *name);
static void              preferences_callback_entry_free           (PreferencesCallbackEntry *callback_entry);
static void              preferences_entry_update_auto_storage     (PreferencesEntry         *entry);
static PreferencesEntry *preferences_global_table_lookup_or_insert (const char               *name);

static int
preferences_mateconf_value_get_int (const MateConfValue *value)
{
    if (value == NULL)
    {
        return 0;
    }
    g_assert (value->type == MATECONF_VALUE_INT);
    return mateconf_value_get_int (value);
}

static gboolean
preferences_mateconf_value_get_bool (const MateConfValue *value)
{
    if (value == NULL)
    {
        return FALSE;
    }
    g_assert (value->type == MATECONF_VALUE_BOOL);
    return mateconf_value_get_bool (value);
}

static char *
preferences_mateconf_value_get_string (const MateConfValue *value)
{
    if (value == NULL)
    {
        return g_strdup ("");
    }
    g_assert (value->type == MATECONF_VALUE_STRING);
    return g_strdup (mateconf_value_get_string (value));
}

static char **
preferences_mateconf_value_get_string_array (const MateConfValue *value)
{
    GSList *slist, *l;
    GPtrArray *result;

    if (value == NULL)
    {
        return NULL;
    }

    g_assert (value->type == MATECONF_VALUE_LIST);
    g_assert (mateconf_value_get_list_type (value) == MATECONF_VALUE_STRING);

    slist = eel_mateconf_value_get_string_list (value);

    result = g_ptr_array_new ();
    for (l = slist; l != NULL; l = l->next)
    {
        g_ptr_array_add (result, l->data);
    }
    g_slist_free (slist);
    g_ptr_array_add (result, NULL);

    return (char **) g_ptr_array_free (result, FALSE);
}

static const char *
preferences_peek_storage_path (void)
{
    g_assert (storage_path != NULL);

    return storage_path;
}

static void
preferences_set_storage_path (const char *new_storage_path)
{
    g_assert (eel_strlen (new_storage_path) > 0);

    /* Make sure the path is indeed different */
    if (eel_str_is_equal (new_storage_path, storage_path))
    {
        return;
    }

    /* Free the preference hash table */
    preferences_global_table_free ();

    /* Stop monitoring the old path */
    eel_mateconf_monitor_remove (storage_path);

    g_free (storage_path);
    storage_path = g_strdup (new_storage_path);

    /* Start monitoring the new path */
    eel_mateconf_monitor_add (storage_path);
}

static gboolean
preferences_is_initialized (void)
{
    return initialized;
}

static MateConfValue *
preferences_get_value (const char *name)
{
    MateConfValue *result;
    char *key;
    PreferencesEntry *entry;

    g_assert (name != NULL);
    g_assert (preferences_is_initialized ());

    key = preferences_key_make (name);
    result = eel_mateconf_get_value (key);
    g_free (key);

    if (result == NULL)
    {
        entry = preferences_global_table_lookup_or_insert (name);

        if (entry->fallback)
            result = mateconf_value_copy (entry->fallback);
    }

    return result;
}

/* If the preference name begind with a "/", we interpret
 * it as a straight mateconf key. */
static gboolean
preferences_preference_is_mateconf_key (const char *name)
{
    g_assert (name != NULL);

    if (eel_str_has_prefix (name, "/"))
    {
        return FALSE;
    }

    return TRUE;
}

static char *
preferences_key_make (const char *name)
{
    g_assert (name != NULL);

    if (!preferences_preference_is_mateconf_key (name))
    {
        return g_strdup (name);
    }

    /* Otherwise, we prefix it with the path */
    return g_strconcat (preferences_peek_storage_path (), "/",
                        name, NULL);
}

/* Get default from schema or emergency fallback */
static MateConfValue *
preferences_get_default_value (const char *name)
{
    MateConfValue *result;
    PreferencesEntry *entry;
    char *key;

    g_assert (name != NULL);

    key = preferences_key_make (name);

    result = eel_mateconf_get_default_value (key);

    g_free (key);

    if (result == NULL)
    {
        entry = preferences_global_table_lookup_or_insert (name);
        if (entry && entry->fallback)
            result = mateconf_value_copy (entry->fallback);
    }

    return result;
}

static int
preferences_callback_entry_compare (gconstpointer a,
                                    gconstpointer b)
{
    const PreferencesCallbackEntry *a_entry = a;
    const PreferencesCallbackEntry *b_entry = b;

    if (a_entry->callback < b_entry->callback)
    {
        return -1;
    }

    if (a_entry->callback > b_entry->callback)
    {
        return +1;
    }

    if (a_entry->callback_data < b_entry->callback_data)
    {
        return -1;
    }

    if (a_entry->callback_data > b_entry->callback_data)
    {
        return +1;
    }

    return 0;
}

/* Public preferences functions */

gboolean
eel_preferences_get_is_invisible (const char *name)
{
    g_assert (name != NULL);
    g_assert (preferences_is_initialized ());

    return preferences_global_table_lookup_or_insert (name)->invisible;
}

void
eel_preferences_set_is_invisible (const char *name,
                                  gboolean is_invisible)
{
    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    preferences_global_table_lookup_or_insert (name)->invisible = is_invisible;
}

void
eel_preferences_set_boolean (const char *name,
                             gboolean boolean_value)
{
    char *key;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    key = preferences_key_make (name);
    eel_mateconf_set_boolean (key, boolean_value);
    g_free (key);

    eel_mateconf_suggest_sync ();
}

gboolean
eel_preferences_get_boolean (const char *name)
{
    gboolean result;
    MateConfValue *value;

    g_return_val_if_fail (name != NULL, 0);
    g_return_val_if_fail (preferences_is_initialized (), 0);

    value = preferences_get_value (name);
    result = preferences_mateconf_value_get_bool (value);
    eel_mateconf_value_free (value);

    return result;
}

void
eel_preferences_set_integer (const char *name,
                             int int_value)
{
    char *key;
    int old_value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    key = preferences_key_make (name);
    old_value = eel_preferences_get_integer (name);

    if (int_value != old_value)
    {
        eel_mateconf_set_integer (key, int_value);
    }
    g_free (key);
}

int
eel_preferences_get_integer (const char *name)
{
    int result;
    MateConfValue *value;

    g_return_val_if_fail (name != NULL, 0);
    g_return_val_if_fail (preferences_is_initialized (), 0);

    value = preferences_get_value (name);
    result = preferences_mateconf_value_get_int (value);
    eel_mateconf_value_free (value);

    return result;
}

/* MateConf has no unsigned store, save as signed and cast */
guint
eel_preferences_get_uint (const char *name)
{
    return (guint)eel_preferences_get_integer (name);
}
void
eel_preferences_set_uint (const char              *name,
                          guint                    uint_value)
{
    eel_preferences_set_integer (name, (int)uint_value);
}

guint
eel_preferences_get_enum (const char *name)
{
    guint             ret_val;
    char             *str_value;
    MateConfValue       *value;
    const EelEnumeration   *enumeration;
    PreferencesEntry *entry;

    g_return_val_if_fail (name != NULL, 0);
    g_return_val_if_fail (preferences_is_initialized (), 0);

    entry = preferences_global_table_lookup_or_insert (name);
    g_return_val_if_fail (entry != NULL, 0);

    enumeration = eel_enumeration_lookup (entry->enumeration_id);

    if (!enumeration)
    {
        g_warning ("No enum entry for '%s' (%s)",
                   name, entry->enumeration_id);
        return 0;
    }

    value = preferences_get_value (name);
    if (value->type == MATECONF_VALUE_INT)   /* compatibility path */
    {
        ret_val = (guint)preferences_mateconf_value_get_int (value);
        eel_mateconf_value_free (value);
        return ret_val;
    }

    str_value = preferences_mateconf_value_get_string (value);
    eel_mateconf_value_free (value);

    if (str_value == NULL)
    {
        g_warning ("No key for '%s' at %s", str_value, name);
        return 0;
    }

    ret_val = eel_enumeration_get_value_for_name (enumeration, str_value);

    g_free (str_value);

    return ret_val;
}

void
eel_preferences_set_enum (const char *name,
                          guint       int_value)
{
    const char       *str_value;
    const EelEnumeration   *enumeration;
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_return_if_fail (entry != NULL);

    enumeration = eel_enumeration_lookup (entry->enumeration_id);

    if (!enumeration)
    {
        g_warning ("No enum entry for '%s' (%s)",
                   name, entry->enumeration_id);
        return;
    }

    str_value = eel_enumeration_get_name_for_value (enumeration, int_value);

    if (str_value == NULL)
    {
        g_warning ("No enum match for '%d'", int_value);
        return;
    }

    eel_preferences_set (name, str_value);
}

void
eel_preferences_set (const char *name,
                     const char *string_value)
{
    char *key;
    char *old_value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    key = preferences_key_make (name);
    old_value = eel_preferences_get (name);

    if (strcmp (string_value, old_value) != 0)
    {
        eel_mateconf_set_string (key, string_value);
    }
    g_free (key);
    g_free (old_value);
}

char *
eel_preferences_get (const char *name)
{
    char *result;
    MateConfValue *value;

    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (preferences_is_initialized (), NULL);

    value = preferences_get_value (name);
    result = preferences_mateconf_value_get_string (value);
    eel_mateconf_value_free (value);

    return result;
}

void
eel_preferences_set_string_array (const char  *name,
                                  char       **strv_value)
{
    GSList *slist;
    int i;
    char *key;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    slist = NULL;
    if (strv_value != NULL)
    {
        for (i = 0; strv_value[i] != NULL; i++)
        {
            slist = g_slist_prepend (slist, strv_value[i]);
        }
        slist = g_slist_reverse (slist);
    }

    key = preferences_key_make (name);
    eel_mateconf_set_string_list (key, slist);
    g_free (key);

    g_slist_free (slist);
}

static gboolean
string_array_is_valid (char **strv, const char *enumeration_id)
{
    guint i;
    gboolean res;

    g_assert (strv != NULL);
    g_assert (enumeration_id != NULL);

    res = TRUE;
    for (i = 0; strv[i] != NULL; i++)
    {
        const EelEnumeration *enumeration;

        enumeration = eel_enumeration_lookup (enumeration_id);
        if (!enumeration)
        {
            res = FALSE;
            break;
        }

        if (!eel_enumeration_contains_name (enumeration, strv[i]))
        {
            res = FALSE;
            break;
        }
    }

    return res;
}

char **
eel_preferences_get_string_array (const char *name)
{
    char **result;
    MateConfValue *value;
    PreferencesEntry *entry;
    MateConfValue *default_value;

    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (preferences_is_initialized (), NULL);

    value = preferences_get_value (name);
    result = preferences_mateconf_value_get_string_array (value);
    eel_mateconf_value_free (value);

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    /* No enumeration_id so we're done */
    if (entry->enumeration_id == NULL)
    {
        return result;
    }

    /* Do a sanity check on the validity of the values */
    if (string_array_is_valid (result, entry->enumeration_id))
    {
        return result;
    }

    /* Forget the bad value and use the default instead */
    g_strfreev (result);

    default_value = preferences_get_default_value (name);
    if (default_value)
    {
        result = preferences_mateconf_value_get_string_array (default_value);
        mateconf_value_free (default_value);
    }

    return result;
}

void
eel_preferences_unset (const char *name)
{
    char *key;

    g_return_if_fail (name != NULL);
    g_return_if_fail (preferences_is_initialized ());

    key = preferences_key_make (name);

    eel_mateconf_unset (key);

    g_free (key);
}

gboolean
eel_preferences_key_is_writable (const char *name)
{
    gboolean result;
    char *key;

    g_return_val_if_fail (name != NULL, 0);
    g_return_val_if_fail (preferences_is_initialized (), 0);

    key = preferences_key_make (name);
    result = eel_mateconf_key_is_writable (key);
    g_free (key);

    return result;
}

/**
 * preferences_callback_entry_invoke_function
 *
 * A function that invokes a callback from the given struct.  It is meant to be fed to
 * g_list_foreach ()
 * @data: The list data privately maintained by the GList.
 * @callback_data: The callback_data privately maintained by the GList.
 **/
static void
preferences_callback_entry_invoke_function (gpointer data,
        gpointer callback_data)
{
    PreferencesCallbackEntry *callback_entry;

    g_assert (data != NULL);

    callback_entry = data;

    (* callback_entry->callback) (callback_entry->callback_data);
}

static void
preferences_entry_invoke_callbacks (PreferencesEntry *entry)
{
    g_assert (entry != NULL);

    /* Update the auto storage preferences */
    if (entry->auto_storage_list != NULL)
    {
        preferences_entry_update_auto_storage (entry);
    }

    /* Invoke callbacks for this entry if any */
    if (entry->callback_list != NULL)
    {
        g_list_foreach (entry->callback_list,
                        preferences_callback_entry_invoke_function,
                        NULL);
    }
}

static void
update_auto_string (gpointer data, gpointer callback_data)
{
    char **storage;
    const char *value;

    g_assert (data != NULL);
    g_assert (callback_data != NULL);

    storage = (char **)data;
    value = (const char *)callback_data;

    g_free (*storage);
    *(char **)storage = g_strdup (value);
}

static void
update_auto_string_array (gpointer data, gpointer callback_data)
{
    char ***storage;
    char **value;

    g_assert (data != NULL);
    g_assert (callback_data != NULL);

    storage = (char ***)data;
    value = (char **)callback_data;

    g_strfreev (*storage);
    *(char ***)storage = value ? g_strdupv (value) : NULL;
}

static void
update_auto_string_array_as_quarks (gpointer data, gpointer callback_data)
{
    GQuark **storage;
    char **value;
    int i = 0;

    g_assert (data != NULL);
    g_assert (callback_data != NULL);

    storage = (GQuark **)data;
    value = (char **)callback_data;

    g_free (*storage);
    *storage = g_new (GQuark, g_strv_length (value) + 1);

    if (value != NULL)
    {
        for (i = 0; value[i] != NULL; ++i)
        {
            (*storage)[i] = g_quark_from_string (value[i]);
        }
    }
    (*storage)[i] = 0;
}

static void
update_auto_integer_or_boolean (gpointer data, gpointer callback_data)
{
    g_assert (data != NULL);

    *(int *)data = GPOINTER_TO_INT (callback_data);
}

static void
preferences_entry_update_auto_storage (PreferencesEntry *entry)
{
    char *new_string_value;
    char **new_string_array_value;
    int new_int_value;
    guint new_uint_value;
    gboolean new_boolean_value;

    switch (entry->type)
    {
    case PREFERENCE_STRING:
        if (entry->enumeration_id != NULL)
        {
            new_uint_value = eel_preferences_get_enum (entry->name);
            g_list_foreach (entry->auto_storage_list,
                            update_auto_integer_or_boolean,
                            GINT_TO_POINTER (new_uint_value));
        }
        else
        {
            new_string_value = eel_preferences_get (entry->name);
            g_list_foreach (entry->auto_storage_list,
                            update_auto_string,
                            new_string_value);
            g_free (new_string_value);
        }
        break;
    case PREFERENCE_STRING_ARRAY:
        new_string_array_value = eel_preferences_get_string_array (entry->name);
        g_list_foreach (entry->auto_storage_list,
                        update_auto_string_array,
                        new_string_array_value);
        g_strfreev (new_string_array_value);
        break;
    case PREFERENCE_STRING_ARRAY_AS_QUARKS:
        new_string_array_value = eel_preferences_get_string_array (entry->name);
        g_list_foreach (entry->auto_storage_list,
                        update_auto_string_array_as_quarks,
                        new_string_array_value);
        g_strfreev (new_string_array_value);
        break;
    case PREFERENCE_INTEGER:
        new_int_value = eel_preferences_get_integer (entry->name);
        g_list_foreach (entry->auto_storage_list,
                        update_auto_integer_or_boolean,
                        GINT_TO_POINTER (new_int_value));
        break;
    case PREFERENCE_BOOLEAN:
        new_boolean_value = eel_preferences_get_boolean (entry->name);
        g_list_foreach (entry->auto_storage_list,
                        update_auto_integer_or_boolean,
                        GINT_TO_POINTER (new_boolean_value));
        break;
    default:
        g_warning ("unexpected preferences type %d in preferences_entry_update_auto_storage", entry->type);
    }
}

static void
preferences_something_changed_notice (MateConfClient *client,
                                      guint connection_id,
                                      MateConfEntry *entry,
                                      gpointer notice_data)
{
    g_assert (entry != NULL);
    g_assert (entry->key != NULL);
    g_assert (notice_data != NULL);

    preferences_entry_invoke_callbacks (notice_data);
}

static void
preferences_entry_ensure_mateconf_connection (PreferencesEntry *entry)
{
    char *key;

    /*
     * We install only one mateconf notification for each preference entry.
     * Otherwise, we would invoke the installed callbacks more than once
     * per registered callback.
     */
    if (entry->mateconf_connection_id != EEL_MATECONF_UNDEFINED_CONNECTION)
    {
        return;
    }

    g_assert (entry->name != NULL);

    key = preferences_key_make (entry->name);

    entry->mateconf_connection_id = eel_mateconf_notification_add (key,
                                    preferences_something_changed_notice,
                                    entry);
    g_free (key);

    g_assert (entry->mateconf_connection_id != EEL_MATECONF_UNDEFINED_CONNECTION);
}

/**
 * preferences_entry_add_callback
 *
 * Add a callback to a pref node.  Callbacks are fired whenever
 * the pref value changes.
 * @preferences_entry: The hash node.
 * @callback: The user-supplied callback.
 * @callback_data: The user-supplied closure.
 **/
static void
preferences_entry_add_callback (PreferencesEntry *entry,
                                EelPreferencesCallback callback,
                                gpointer callback_data)
{
    PreferencesCallbackEntry *callback_entry;
    GList *l;

    g_assert (entry != NULL);
    g_assert (callback != NULL);

    callback_entry = g_new0 (PreferencesCallbackEntry, 1);
    callback_entry->callback = callback;
    callback_entry->callback_data = callback_data;

    l = g_list_find_custom (entry->callback_list, callback_entry, preferences_callback_entry_compare);
    if (l == NULL)
    {
        entry->callback_list = g_list_append (entry->callback_list, callback_entry);
        preferences_entry_ensure_mateconf_connection (entry);
    }
    else
    {
        g_warning ("Trying to add a callback for %s that already exists.", entry->name);
    }
}

/**
 * preferences_entry_add_auto_storage
 *
 * Add an auto-storage variable to a pref node.  The variable will
 * be updated to match the pref value whenever the pref
 * the pref value changes.
 * @preferences_entry: The hash node.
 * @storage: The user-supplied location at which to store the value.
 * @type: Which type of variable this is.
 **/
static void
preferences_entry_add_auto_storage (PreferencesEntry *entry,
                                    gpointer storage,
                                    PreferenceType type)
{
    g_assert (entry != NULL);
    g_assert (storage != NULL);
    g_assert (entry->type == 0 || entry->type == type);
    if (g_list_find (entry->auto_storage_list, storage) != NULL)
    {
        g_warning ("Trying to add an auto storage for %s that already exists.", entry->name);
        return;
    }

    entry->type = type;

    entry->auto_storage_list = g_list_append (entry->auto_storage_list, storage);

    preferences_entry_ensure_mateconf_connection (entry);
}

static void
preferences_entry_check_remove_connection (PreferencesEntry *entry)
{
    /*
     * If there are no callbacks or auto-storage variables left in the entry,
     * remove the mateconf notification.
     */
    if (entry->callback_list != NULL || entry->auto_storage_list != NULL)
    {
        return;
    }

    eel_mateconf_notification_remove (entry->mateconf_connection_id);
    entry->mateconf_connection_id = EEL_MATECONF_UNDEFINED_CONNECTION;
}

/**
 * preferences_entry_remove_callback
 *
 * remove a callback from a pref entry.  Both the callback and the callback_data must
 * match in order for a callback to be removed from the entry.
 * @preferences_entry: The hash entry.
 * @callback: The user-supplied callback.
 * @callback_data: The user-supplied closure.
 **/
static void
preferences_entry_remove_callback (PreferencesEntry *entry,
                                   EelPreferencesCallback callback,
                                   gpointer callback_data)
{
    PreferencesCallbackEntry cb_entry;
    GList *l;

    g_assert (entry != NULL);
    g_assert (callback != NULL);

    cb_entry.callback = callback;
    cb_entry.callback_data = callback_data;

    l = g_list_find_custom (entry->callback_list, &cb_entry, preferences_callback_entry_compare);
    if (l != NULL)
    {
        preferences_callback_entry_free (l->data);
        entry->callback_list = g_list_delete_link (entry->callback_list, l);
        preferences_entry_check_remove_connection (entry);
    }
    else
    {
        g_warning ("Trying to remove a callback for %s without adding it first.", entry->name);
    }

    g_assert (g_list_find_custom (entry->callback_list, &cb_entry, preferences_callback_entry_compare) == NULL);
}

/**
 * preferences_entry_remove_auto_storage
 *
 * remove an auto-storage variable from a pref entry.
 * @preferences_entry: The hash entry.
 * @storage: The user-supplied location.
 **/
static void
preferences_entry_remove_auto_storage (PreferencesEntry *entry,
                                       gpointer storage)
{
    GList *new_list;
    const GList *node;
    gpointer storage_in_entry;

    g_assert (entry != NULL);
    g_assert (storage != NULL);
    g_assert (entry->auto_storage_list != NULL);

    new_list = g_list_copy (entry->auto_storage_list);

    for (node = new_list; node != NULL; node = node->next)
    {
        storage_in_entry = node->data;

        g_assert (storage_in_entry != NULL);

        if (storage_in_entry == storage)
        {
            entry->auto_storage_list = g_list_remove (entry->auto_storage_list,
                                       storage);

            switch (entry->type)
            {
            case PREFERENCE_STRING:
                update_auto_string (storage, NULL);
                break;
            case PREFERENCE_STRING_ARRAY:
                update_auto_string_array (storage, NULL);
                break;
            case PREFERENCE_STRING_ARRAY_AS_QUARKS:
                update_auto_string_array_as_quarks (storage, NULL);
                break;
            case PREFERENCE_BOOLEAN:
            case PREFERENCE_INTEGER:
                update_auto_integer_or_boolean (storage, NULL);
                break;
            default:
                g_warning ("unexpected preference type %d in preferences_entry_remove_auto_storage", entry->type);
            }
        }
    }

    g_list_free (new_list);

    preferences_entry_check_remove_connection (entry);
}

/**
 * preferences_callback_entry_free
 *
 * Free a callback info struct.
 * @preferences_callback_entry: The struct to free.
 **/
static void
preferences_callback_entry_free (PreferencesCallbackEntry *callback_entry)
{
    g_assert (callback_entry != NULL);

    callback_entry->callback = NULL;
    callback_entry->callback_data = NULL;

    g_free (callback_entry);
}

/**
 * preferences_callback_entry_free_func
 *
 * A function that frees a callback info struct.  It is meant to be fed to
 * g_list_foreach ()
 * @data: The list data privately maintained by the GList.
 * @callback_data: The callback_data privately maintained by the GList.
 **/
static void
preferences_callback_entry_free_func (gpointer	data,
                                      gpointer	callback_data)
{
    g_assert (data != NULL);

    preferences_callback_entry_free (data);
}

/**
 * preferences_entry_free
 *
 * Free a preference hash node's members along with the node itself.
 * @preferences_hash_node: The node to free.
 **/
static void
preferences_entry_free (PreferencesEntry *entry)
{
    g_assert (entry != NULL);

    eel_mateconf_notification_remove (entry->mateconf_connection_id);
    entry->mateconf_connection_id = EEL_MATECONF_UNDEFINED_CONNECTION;

    g_list_free (entry->auto_storage_list);
    eel_g_list_free_deep_custom (entry->callback_list,
                                 preferences_callback_entry_free_func,
                                 NULL);

    entry->auto_storage_list = NULL;
    entry->callback_list = NULL;

    g_free (entry->name);
    g_free (entry->description);
    g_free (entry->enumeration_id);

    eel_mateconf_value_free (entry->fallback);

    g_free (entry);
}

/**
 * preferences_entry_free_func
 *
 * A function that frees a pref hash node.  It is meant to be fed to
 * g_hash_table_foreach ()
 * @key: The hash key privately maintained by the GHashTable.
 * @value: The hash value privately maintained by the GHashTable.
 * @callback_data: The callback_data privately maintained by the GHashTable.
 **/
static void
preferences_entry_free_func (gpointer key,
                             gpointer value,
                             gpointer callback_data)
{
    g_assert (value != NULL);

    preferences_entry_free (value);
}

static void
preferences_global_table_free (void)
{
    if (global_table == NULL)
    {
        return;
    }

    g_hash_table_foreach (global_table, preferences_entry_free_func, NULL);
    g_hash_table_destroy (global_table);
    global_table = NULL;

    g_free (storage_path);
    storage_path = NULL;
}

static void
preferences_uninitialize (void)
{
    initialized = FALSE;
}

static GHashTable *
preferences_global_table_get_global (void)
{
    static gboolean at_exit_handler_added = FALSE;

    if (global_table == NULL)
    {
        global_table = g_hash_table_new (g_str_hash, g_str_equal);

        if (!at_exit_handler_added)
        {
            at_exit_handler_added = TRUE;
            eel_debug_call_at_shutdown (preferences_global_table_free);
            /* ensure that we catch calls to preferences functions after eel shutdown */
            eel_debug_call_at_shutdown (preferences_uninitialize);
        }
    }

    return global_table;
}

static PreferencesEntry *
preferences_global_table_lookup (const char *name)
{
    g_assert (name != NULL);
    g_assert (preferences_global_table_get_global () != NULL);

    return g_hash_table_lookup (preferences_global_table_get_global (), name);
}

static PreferencesEntry *
preferences_global_table_insert (const char *name)
{
    PreferencesEntry *entry;

    g_assert (name != NULL);
    g_assert (preferences_global_table_get_global () != NULL);
    g_assert (preferences_global_table_lookup (name) == NULL);

    entry = g_new0 (PreferencesEntry, 1);
    entry->name = g_strdup (name);

    g_hash_table_insert (preferences_global_table_get_global (), entry->name, entry);

    g_assert (entry == preferences_global_table_lookup (name));

    return entry;
}

static PreferencesEntry *
preferences_global_table_lookup_or_insert (const char *name)
{
    PreferencesEntry *entry;

    g_assert (name != NULL);

    entry = preferences_global_table_lookup (name);

    if (entry != NULL)
    {
        return entry;
    }

    entry = preferences_global_table_insert (name);
    g_assert (entry != NULL);

    return entry;
}

void
eel_preferences_add_callback (const char *name,
                              EelPreferencesCallback callback,
                              gpointer callback_data)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (callback != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_callback (entry, callback, callback_data);
}

void
eel_preferences_add_auto_string (const char *name,
                                 const char **storage)
{
    PreferencesEntry *entry;
    char *value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_STRING);

    value = eel_preferences_get (entry->name);
    update_auto_string (storage, value);
    g_free (value);
}

void
eel_preferences_add_auto_string_array (const char   *name,
                                       char       ***storage)
{
    PreferencesEntry *entry;
    char **value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_STRING_ARRAY);

    value = eel_preferences_get_string_array (entry->name);
    update_auto_string_array (storage, value);
    g_strfreev (value);
}

void
eel_preferences_add_auto_string_array_as_quarks (const char *name,
        GQuark **storage)
{
    PreferencesEntry *entry;
    char **value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_STRING_ARRAY_AS_QUARKS);

    value = eel_preferences_get_string_array (entry->name);
    update_auto_string_array_as_quarks (storage, value);
    g_strfreev (value);
}

void
eel_preferences_add_auto_integer (const char *name,
                                  int *storage)
{
    PreferencesEntry *entry;
    int value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_INTEGER);

    value = eel_preferences_get_integer (entry->name);
    update_auto_integer_or_boolean (storage, GINT_TO_POINTER (value));
}


void
eel_preferences_add_auto_enum (const char *name,
                               guint *storage)
{
    PreferencesEntry *entry;
    guint value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);
    g_assert (entry->enumeration_id != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_STRING);

    value = eel_preferences_get_enum (entry->name);
    update_auto_integer_or_boolean (storage, GINT_TO_POINTER (value));
}

void
eel_preferences_add_auto_boolean (const char *name,
                                  gboolean *storage)
{
    PreferencesEntry *entry;
    gboolean value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    preferences_entry_add_auto_storage (entry, storage, PREFERENCE_BOOLEAN);

    value = eel_preferences_get_boolean (entry->name);
    update_auto_integer_or_boolean (storage, GINT_TO_POINTER (value));
}

void
eel_preferences_remove_auto_string (const char *name,
                                    const char **storage)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup (name);
    if (entry == NULL)
    {
        g_warning ("Trying to remove auto-string for %s without adding it first.", name);
        return;
    }

    preferences_entry_remove_auto_storage (entry, storage);
}

void
eel_preferences_remove_auto_string_array (const char *name,
        char ***storage)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup (name);
    if (entry == NULL)
    {
        g_warning ("Trying to remove auto-string for %s without adding it first.", name);
        return;
    }

    preferences_entry_remove_auto_storage (entry, storage);
}

void
eel_preferences_remove_auto_integer (const char *name,
                                     int *storage)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup (name);
    if (entry == NULL)
    {
        g_warning ("Trying to remove auto-integer for %s without adding it first.", name);
        return;
    }

    preferences_entry_remove_auto_storage (entry, storage);
}

void
eel_preferences_remove_auto_boolean (const char *name,
                                     gboolean *storage)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (storage != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup (name);
    if (entry == NULL)
    {
        g_warning ("Trying to remove auto-boolean for %s without adding it first.", name);
        return;
    }

    preferences_entry_remove_auto_storage (entry, storage);
}

typedef struct
{
    char *name;
    EelPreferencesCallback callback;
    gpointer callback_data;
} WhileAliveData;

static void
preferences_while_alive_disconnector (gpointer callback_data, GObject *where_object_was)
{
    WhileAliveData *data;

    g_assert (callback_data != NULL);

    data = callback_data;

    /* we might have survived an eel shutdown, which
     * already cleared all the callbacks */
    if (preferences_is_initialized ())
    {
        eel_preferences_remove_callback (data->name,
                                         data->callback,
                                         data->callback_data);
    }

    g_free (data->name);
    g_free (data);
}

void
eel_preferences_add_callback_while_alive (const char *name,
        EelPreferencesCallback callback,
        gpointer callback_data,
        GObject *alive_object)
{
    WhileAliveData *data;

    g_return_if_fail (name != NULL);
    g_return_if_fail (callback != NULL);
    g_return_if_fail (G_IS_OBJECT (alive_object));
    g_return_if_fail (preferences_is_initialized ());

    data = g_new (WhileAliveData, 1);
    data->name = g_strdup (name);
    data->callback = callback;
    data->callback_data = callback_data;

    eel_preferences_add_callback (name, callback, callback_data);

    g_object_weak_ref (alive_object,
                       preferences_while_alive_disconnector,
                       data);
}

void
eel_preferences_remove_callback (const char *name,
                                 EelPreferencesCallback callback,
                                 gpointer callback_data)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (callback != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup (name);

    if (entry == NULL)
    {
        g_warning ("Trying to remove a callback for %s without adding it first.", name);
        return;
    }

    preferences_entry_remove_callback (entry, callback, callback_data);
}

void
eel_preferences_set_description (const char *name,
                                 const char *description)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (description != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    g_free (entry->description);
    entry->description = g_strdup (description);
}

char *
eel_preferences_get_description (const char *name)
{
    PreferencesEntry *entry;

    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (preferences_is_initialized (), NULL);

    entry = preferences_global_table_lookup_or_insert (name);

    return g_strdup (entry->description ? entry->description : "");
}

void
eel_preferences_set_enumeration_id (const char *name,
                                    const char *enumeration_id)
{
    PreferencesEntry *entry;

    g_return_if_fail (name != NULL);
    g_return_if_fail (enumeration_id != NULL);
    g_return_if_fail (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    g_free (entry->enumeration_id);
    entry->enumeration_id = g_strdup (enumeration_id);
}

char *
eel_preferences_get_enumeration_id (const char *name)
{
    PreferencesEntry *entry;

    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (preferences_is_initialized (), NULL);

    entry = preferences_global_table_lookup_or_insert (name);

    return g_strdup (entry->enumeration_id);
}

static void
preferences_set_emergency_fallback_stealing_value (const char *name,
        MateConfValue *value)
{
    PreferencesEntry *entry;

    g_assert (name != NULL);
    g_assert (preferences_is_initialized ());

    entry = preferences_global_table_lookup_or_insert (name);
    g_assert (entry != NULL);

    if (entry->fallback)
        mateconf_value_free (entry->fallback);
    entry->fallback = value; /* steal ownership of value */
}

void
eel_preferences_set_emergency_fallback_string (const char *name,
        const char *value)
{
    MateConfValue *mateconf_value;

    g_return_if_fail (name != NULL);
    g_return_if_fail (value != NULL);

    mateconf_value = mateconf_value_new (MATECONF_VALUE_STRING);

    mateconf_value_set_string (mateconf_value, value);

    preferences_set_emergency_fallback_stealing_value (name, mateconf_value);
}

void
eel_preferences_set_emergency_fallback_integer (const char *name,
        int         value)
{
    MateConfValue *mateconf_value;

    g_return_if_fail (name != NULL);

    mateconf_value = mateconf_value_new (MATECONF_VALUE_INT);

    mateconf_value_set_int (mateconf_value, value);

    preferences_set_emergency_fallback_stealing_value (name, mateconf_value);
}

void
eel_preferences_set_emergency_fallback_boolean (const char *name,
        gboolean    value)
{
    MateConfValue *mateconf_value;

    g_return_if_fail (name != NULL);

    mateconf_value = mateconf_value_new (MATECONF_VALUE_BOOL);

    mateconf_value_set_bool (mateconf_value, value);

    preferences_set_emergency_fallback_stealing_value (name, mateconf_value);
}


void
eel_preferences_set_emergency_fallback_string_array (const char  *name,
        char       **value)
{
    MateConfValue *mateconf_value;
    GSList *list;
    int i;

    g_return_if_fail (name != NULL);
    g_return_if_fail (value != NULL);

    mateconf_value = mateconf_value_new (MATECONF_VALUE_LIST);
    mateconf_value_set_list_type (mateconf_value, MATECONF_VALUE_STRING);

    list = NULL;
    for (i = 0; value[i] != NULL; ++i)
    {
        MateConfValue *v;

        v = mateconf_value_new (MATECONF_VALUE_STRING);
        mateconf_value_set_string (v, value[i]);

        list = g_slist_prepend (list, v);
    }

    mateconf_value_set_list_nocopy (mateconf_value, g_slist_reverse (list));

    preferences_set_emergency_fallback_stealing_value (name, mateconf_value);
}

MateConfValue*
eel_preferences_get_emergency_fallback (const char *name)
{
    PreferencesEntry *entry;

    g_return_val_if_fail (name != NULL, NULL);
    g_return_val_if_fail (preferences_is_initialized (), NULL);

    entry = preferences_global_table_lookup_or_insert (name);

    return entry->fallback ? mateconf_value_copy (entry->fallback) : NULL;
}

gboolean
eel_preferences_monitor_directory (const char *directory)
{
    g_return_val_if_fail (preferences_is_initialized (), FALSE);

    return eel_mateconf_monitor_add (directory);
}

gboolean
eel_preferences_is_visible (const char *name)
{
    g_return_val_if_fail (name != NULL, FALSE);
    g_return_val_if_fail (preferences_is_initialized (), FALSE);

    return !preferences_global_table_lookup_or_insert (name)->invisible;
}

void
eel_preferences_init (const char *path)
{
    g_return_if_fail (eel_strlen (path) > 0);

    if (initialized)
    {
        return;
    }

    initialized = TRUE;

    preferences_set_storage_path (path);
}

#if !defined (EEL_OMIT_SELF_CHECK)

#define CHECK_BOOLEAN(name__, value__)								\
G_STMT_START {											\
	eel_preferences_set_boolean ((name__), (value__));					\
	EEL_CHECK_BOOLEAN_RESULT (eel_preferences_get_boolean (name__), (value__));	\
} G_STMT_END

#define CHECK_INTEGER(name__, value__)								\
G_STMT_START {											\
	eel_preferences_set_integer ((name__), (value__));					\
	EEL_CHECK_INTEGER_RESULT (eel_preferences_get_integer (name__), (value__));	\
} G_STMT_END

#define CHECK_STRING(name__, value__)							\
G_STMT_START {										\
	eel_preferences_set ((name__), (value__));					\
	EEL_CHECK_STRING_RESULT (eel_preferences_get (name__), (value__));		\
} G_STMT_END

void
eel_self_check_preferences (void)
{
    /* Disabled until I can debug why these seemingly harmless tests
     * dont work. -re
     */
#if 0
    int original_user_level;

    original_user_level = eel_preferences_get_user_level ();

    EEL_CHECK_INTEGER_RESULT (eel_preferences_get_integer ("self-check/neverset/i"), 0);
    EEL_CHECK_STRING_RESULT (eel_preferences_get ("self-check/neverset/s"), "");
    EEL_CHECK_BOOLEAN_RESULT (eel_preferences_get_boolean ("self-check/neverset/b"), FALSE);

    eel_preferences_set_user_level (0);

    /* FIXME: Fails if you add the commented-out lines. */
    /* CHECK_INTEGER ("self-check/i", 0); */
    CHECK_INTEGER ("self-check/i", 666);
    /* CHECK_BOOLEAN ("self-check/b", FALSE); */
    CHECK_BOOLEAN ("self-check/b", TRUE);
    /* CHECK_STRING ("self-check/s", ""); */
    CHECK_STRING ("self-check/s", "foo");

    /* Restore the original user level */
    eel_preferences_set_user_level (original_user_level);
#endif
}

#endif /* !EEL_OMIT_SELF_CHECK */
