/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
/* vim:set et sts=4: */

#include <string.h>
#include <ibus.h>
#include "config.h"

#define GCONF_PREFIX "/desktop/ibus"

struct _IBusConfigGConf {
    IBusConfigService parent;
    GConfClient *client;
};

struct _IBusConfigGConfClass {
    IBusConfigServiceClass parent;

};

/* functions prototype */
static void         ibus_config_gconf_class_init    (IBusConfigGConfClass   *class);
static void         ibus_config_gconf_init          (IBusConfigGConf        *config);
static void         ibus_config_gconf_destroy       (IBusConfigGConf        *config);
static gboolean     ibus_config_gconf_set_value     (IBusConfigService      *config,
                                                     const gchar            *section,
                                                     const gchar            *name,
                                                     GVariant               *value,
                                                     GError                **error);
static GVariant    *ibus_config_gconf_get_value     (IBusConfigService      *config,
                                                     const gchar            *section,
                                                     const gchar            *name,
                                                     GError                **error);
static GVariant    *ibus_config_gconf_get_values    (IBusConfigService      *config,
                                                     const gchar            *section,
                                                     GError                **error);
static gboolean     ibus_config_gconf_unset_value   (IBusConfigService      *config,
                                                     const gchar            *section,
                                                     const gchar            *name,
                                                     GError                **error);
static GConfValue   *_to_gconf_value                (GVariant               *value);
static GVariant     *_from_gconf_value              (const GConfValue       *gvalue);

G_DEFINE_TYPE (IBusConfigGConf, ibus_config_gconf, IBUS_TYPE_CONFIG_SERVICE)

static void
ibus_config_gconf_class_init (IBusConfigGConfClass *class)
{
    GObjectClass *object_class = G_OBJECT_CLASS (class);

    IBUS_OBJECT_CLASS (object_class)->destroy = (IBusObjectDestroyFunc) ibus_config_gconf_destroy;
    IBUS_CONFIG_SERVICE_CLASS (object_class)->set_value   = ibus_config_gconf_set_value;
    IBUS_CONFIG_SERVICE_CLASS (object_class)->get_value   = ibus_config_gconf_get_value;
    IBUS_CONFIG_SERVICE_CLASS (object_class)->get_values  = ibus_config_gconf_get_values;
    IBUS_CONFIG_SERVICE_CLASS (object_class)->unset_value = ibus_config_gconf_unset_value;
}

static void
_value_changed_cb (GConfClient     *client,
                   const gchar     *key,
                   GConfValue      *value,
                   IBusConfigGConf *config)
{
    gchar *p, *section, *name;

    g_return_if_fail (key != NULL);

    p = g_strdup (key);
    section = p + sizeof (GCONF_PREFIX);
    name = rindex (p, '/') + 1;
    *(name - 1) = '\0';
    
    GVariant *variant;
    if (value) {
        variant = _from_gconf_value (value);
    }
    else {
        /* Use a empty typle for a unset value */
        variant = g_variant_new_tuple (NULL, 0);
    }
    g_return_if_fail (variant != NULL);
    ibus_config_service_value_changed ((IBusConfigService *) config,
                                       section,
                                       name,
                                       variant);
    g_variant_unref (variant);
    g_free (p);
}

static void
ibus_config_gconf_init (IBusConfigGConf *config)
{
    config->client = gconf_client_get_default ();
    gconf_client_add_dir (config->client,
                          GCONF_PREFIX,
                          GCONF_CLIENT_PRELOAD_RECURSIVE,
                          NULL);
    g_signal_connect (config->client, "value-changed", G_CALLBACK (_value_changed_cb), config);
}

static void
ibus_config_gconf_destroy (IBusConfigGConf *config)
{
    if (config->client) {
        g_signal_handlers_disconnect_by_func (config->client, G_CALLBACK (_value_changed_cb), config);
        g_object_unref (config->client);
        config->client = NULL;
    }

    IBUS_OBJECT_CLASS (ibus_config_gconf_parent_class)->destroy ((IBusObject *)config);
}

static GConfValue *
_to_gconf_value (GVariant *value)
{
    GConfValue *gv = NULL;

    switch (g_variant_classify (value)) {
    case G_VARIANT_CLASS_STRING:
        {
            gv = gconf_value_new (GCONF_VALUE_STRING);
            gconf_value_set_string (gv, g_variant_get_string (value, NULL));
        }
        break;
    case G_VARIANT_CLASS_INT32:
        {
            gv = gconf_value_new (GCONF_VALUE_INT);
            gconf_value_set_int (gv, g_variant_get_int32 (value));
        }
        break;
    case G_VARIANT_CLASS_BOOLEAN:
        {
            gv = gconf_value_new (GCONF_VALUE_BOOL);
            gconf_value_set_bool (gv, g_variant_get_boolean (value));
        }
        break;
    case G_VARIANT_CLASS_DOUBLE:
        {
            gv = gconf_value_new (GCONF_VALUE_FLOAT);
            gconf_value_set_float (gv, g_variant_get_double (value));
        }
        break;
    case G_VARIANT_CLASS_ARRAY:
        {
            const GVariantType *element_type = g_variant_type_element (g_variant_get_type (value));

            GConfValueType type = GCONF_VALUE_INVALID;
            if (g_variant_type_equal (element_type, G_VARIANT_TYPE_STRING))
                type = GCONF_VALUE_STRING;
            else if (g_variant_type_equal (element_type, G_VARIANT_TYPE_INT32))
                type = GCONF_VALUE_INT;
            else if (g_variant_type_equal (element_type, G_VARIANT_TYPE_BOOLEAN))
                type = GCONF_VALUE_BOOL;
            else if (g_variant_type_equal (element_type, G_VARIANT_TYPE_DOUBLE))
                type = GCONF_VALUE_FLOAT;
            else
                g_return_val_if_reached (NULL);

            gv = gconf_value_new (GCONF_VALUE_LIST);
            gconf_value_set_list_type (gv, type);

            GSList *elements = NULL;
            GVariantIter iter;
            GVariant *child;
            g_variant_iter_init (&iter, value);
            while ((child = g_variant_iter_next_value (&iter)) != NULL) {
                elements = g_slist_append (elements, _to_gconf_value (child));
                g_variant_unref (child);
            }
            gconf_value_set_list_nocopy (gv, elements);
        }
        break;
    default:
        g_return_val_if_reached (NULL);
    }

    return gv;
}

static GVariant *
_from_gconf_value (const GConfValue *gv)
{
    g_assert (gv != NULL);

    switch (gv->type) {
    case GCONF_VALUE_STRING:
        return g_variant_new_string (gconf_value_get_string (gv));
    case GCONF_VALUE_INT:
        return g_variant_new_int32 (gconf_value_get_int (gv));
    case GCONF_VALUE_FLOAT:
        return g_variant_new_double (gconf_value_get_float (gv));
    case GCONF_VALUE_BOOL:
        return g_variant_new_boolean (gconf_value_get_bool (gv));
    case GCONF_VALUE_LIST:
        {
            GVariantBuilder builder;
            switch (gconf_value_get_list_type (gv)) {
            case GCONF_VALUE_STRING:
                g_variant_builder_init (&builder, G_VARIANT_TYPE("as")); break;
            case GCONF_VALUE_INT:
                g_variant_builder_init (&builder, G_VARIANT_TYPE("ai")); break;
            case GCONF_VALUE_FLOAT:
                g_variant_builder_init (&builder, G_VARIANT_TYPE("ad")); break;
            case GCONF_VALUE_BOOL:
                g_variant_builder_init (&builder, G_VARIANT_TYPE("ab")); break;
                break;
            default:
                g_assert_not_reached ();
            }

            GSList *list = gconf_value_get_list (gv);
            GSList *p = list;
            while (p != NULL) {
                switch (gconf_value_get_list_type (gv)) {
                case GCONF_VALUE_STRING:
                    g_variant_builder_add (&builder, "s", gconf_value_get_string ((GConfValue *)p->data));
                    break;
                case GCONF_VALUE_INT:
                    g_variant_builder_add (&builder, "i", gconf_value_get_int ((GConfValue *)p->data));
                    break;
                case GCONF_VALUE_FLOAT:
                    g_variant_builder_add (&builder, "d", gconf_value_get_float ((GConfValue *)p->data));
                    break;
                case GCONF_VALUE_BOOL:
                    g_variant_builder_add (&builder, "b", gconf_value_get_bool ((GConfValue *)p->data));
                    break;
                default:
                    g_assert_not_reached ();
                }
                p = p->next;
            }
            return g_variant_builder_end (&builder);
        }
    default:
        g_assert_not_reached ();
    }
}


static gboolean
ibus_config_gconf_set_value (IBusConfigService      *config,
                             const gchar            *section,
                             const gchar            *name,
                             GVariant               *value,
                             GError                **error)
{
    gchar *key;
    GConfValue *gv;

    gv = _to_gconf_value (value);
    if (gv == NULL) {
        gchar *str = g_variant_print (value, TRUE);
        g_set_error (error,
                     G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                     "Can not set config value [%s:%s] to %s.",
                     section, name, str);
        g_free (str);
        return FALSE;
    }
    key = g_strdup_printf (GCONF_PREFIX"/%s/%s", section, name);
    gconf_client_set (((IBusConfigGConf *)config)->client, key, gv, error);

    g_free (key);
    gconf_value_free (gv);

    if (*error != NULL) {
        return FALSE;
    }
    return TRUE;
}

static GVariant *
ibus_config_gconf_get_value (IBusConfigService      *config,
                             const gchar            *section,
                             const gchar            *name,
                             GError                **error)
{
    gchar *key = g_strdup_printf (GCONF_PREFIX"/%s/%s", section, name);

    GConfValue *gv = gconf_client_get (((IBusConfigGConf *) config)->client, key, NULL);

    g_free (key);

    if (gv == NULL) {
        g_set_error (error,
                     G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                     "Config value [%s:%s] does not exist.", section, name);
        return NULL;
    }

    GVariant *variant = _from_gconf_value (gv);
    gconf_value_free (gv);

    return g_variant_ref_sink (variant);
}

static GVariant *
ibus_config_gconf_get_values (IBusConfigService      *config,
                              const gchar            *section,
                              GError                **error)
{
    gchar *dir = g_strdup_printf (GCONF_PREFIX"/%s", section);
    gint len = strlen(dir) + 1;
    GSList *entries = gconf_client_all_entries (((IBusConfigGConf *) config)->client, dir, NULL);
    g_free (dir);

    GSList *p;
    GVariantBuilder builder;

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
    for (p = entries; p != NULL; p = p->next) {
        GConfEntry *entry = (GConfEntry *)p->data;
        if (entry->key != NULL && entry->value != NULL) {
            const gchar *name = entry->key + len;
            GVariant *value = _from_gconf_value (entry->value);
            g_variant_builder_add (&builder, "{sv}", name, value);
        }
        gconf_entry_free (entry);
    }
    g_slist_free (entries);

    return g_variant_builder_end (&builder);
}

static gboolean
ibus_config_gconf_unset_value (IBusConfigService      *config,
                               const gchar            *section,
                               const gchar            *name,
                               GError                **error)
{
    gchar *key = g_strdup_printf (GCONF_PREFIX"/%s/%s", section, name);

    gboolean retval = gconf_client_unset (((IBusConfigGConf *)config)->client, key, error);
    g_free (key);

    return retval;
}

IBusConfigGConf *
ibus_config_gconf_new (GDBusConnection *connection)
{
    IBusConfigGConf *config;
    config = (IBusConfigGConf *) g_object_new (IBUS_TYPE_CONFIG_GCONF,
                                               "object-path", IBUS_PATH_CONFIG,
                                               "connection", connection,
                                               NULL);
    return config;
}
