/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2011, 2014 Red Hat, Inc.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>

#include <gio/gio.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "cc-keyboard-item.h"
#include "cc-util.h"
#include "keyboard-shortcuts.h"

#define CUSTOM_KEYS_SCHEMA "org.gnome.settings-daemon.plugins.media-keys.custom-keybinding"
#define GLOBAL_SHORTCUTS_APP_SCHEMA "org.gnome.settings.global-shortcuts.application"
#define GLOBAL_SHORTCUTS_BINDINGS_SCHEMA "org.gnome.settings.global-shortcuts.application.binding"
#define GLOBAL_SHORTCUTS_PATH "/org/gnome/global-shortcuts/"
#define SHORTCUT_DELIMITERS "+ "

struct _CcKeyboardItem {
    GObject parent_instance;

    CcKeyboardItem *reverse_item;
    gboolean is_reversed;
    gboolean hidden;

    CcKeyboardItemType type;

    BindingGroupType group;
    char *description;
    gboolean editable;
    GList *key_combos;
    GList *default_combos;
    gboolean can_set_multiple;

    /* GSettings path */
    char *gsettings_path;
    gboolean desc_editable;
    char *command;
    gboolean cmd_editable;

    /* GSettings */
    char *schema;
    char *key;
    GSettings *settings;
};

enum {
    PROP_0,
    PROP_DESCRIPTION,
    PROP_EDITABLE,
    PROP_TYPE,
    PROP_IS_VALUE_DEFAULT,
    PROP_COMMAND,
    PROP_KEY_COMBOS,
    N_PROPS
};

static GParamSpec *properties[N_PROPS];

static void cc_keyboard_item_class_init (CcKeyboardItemClass *klass);
static void cc_keyboard_item_init (CcKeyboardItem *keyboard_item);
static void cc_keyboard_item_finalize (GObject *object);

G_DEFINE_FINAL_TYPE (CcKeyboardItem, cc_keyboard_item, G_TYPE_OBJECT)

static const CcKeyCombo EMPTY_COMBO = { 0, 0, 0 };

static gboolean
combo_equal (CcKeyCombo *a, CcKeyCombo *b)
{
    return (a->keyval == b->keyval && a->keycode == b->keycode && a->mask == b->mask);
}

static gboolean
combos_contains (GList *combos, CcKeyCombo *needle)
{
    for (GList *l = combos; l != NULL; l = l->next) {
        if (combo_equal (l->data, needle))
            return TRUE;
    }

    return FALSE;
}

static gboolean
combos_equal (GList *a, GList *b)
{
    // Should be efficient enough for any sane number of bindings

    for (GList *l = a; l != NULL; l = l->next) {
        if (!combos_contains (b, l->data))
            return FALSE;
    }

    for (GList *l = b; l != NULL; l = l->next) {
        if (!combos_contains (a, l->data))
            return FALSE;
    }

    return TRUE;
}

static gboolean
binding_from_string (const char *str, CcKeyCombo *combo)
{
    g_return_val_if_fail (combo != NULL, FALSE);
    g_autofree guint *keycodes = NULL;

    if (str == NULL || strcmp (str, "disabled") == 0) {
        memset (combo, 0, sizeof (CcKeyCombo));
        return TRUE;
    }

    gtk_accelerator_parse_with_keycode (str, gdk_display_get_default (), &combo->keyval, &keycodes, &combo->mask);

    combo->keycode = (keycodes ? keycodes[0] : 0);

    if (combo->keyval == 0)
        return FALSE;
    else
        return TRUE;
}

const char *
cc_keyboard_item_get_global_shortcut_name (CcKeyboardItem *item)
{
    if (item->type == CC_KEYBOARD_ITEM_TYPE_GLOBAL_SHORTCUT)
        return item->key;

    return NULL;
}

static void
_set_description (CcKeyboardItem *item, const char *value)
{
    g_set_str (&item->description, value);
}

const char *
cc_keyboard_item_get_description (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);

    return item->description;
}

gboolean
cc_keyboard_item_get_desc_editable (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE);

    return item->desc_editable;
}

static void
_set_type (CcKeyboardItem *item, gint value)
{
    item->type = value;
}

static void
_set_command (CcKeyboardItem *item, const char *value)
{
    g_set_str (&item->command, value);
}

const char *
cc_keyboard_item_get_command (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);

    return item->command;
}

gboolean
cc_keyboard_item_get_cmd_editable (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE);

    return item->cmd_editable;
}

static void
cc_keyboard_item_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    CcKeyboardItem *self;

    self = CC_KEYBOARD_ITEM (object);

    switch (prop_id) {
    case PROP_DESCRIPTION:
        _set_description (self, g_value_get_string (value));
        break;
    case PROP_COMMAND:
        _set_command (self, g_value_get_string (value));
        break;
    case PROP_TYPE:
        _set_type (self, g_value_get_int (value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
cc_keyboard_item_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    CcKeyboardItem *self;

    self = CC_KEYBOARD_ITEM (object);

    switch (prop_id) {
    case PROP_DESCRIPTION:
        g_value_set_string (value, self->description);
        break;
    case PROP_EDITABLE:
        g_value_set_boolean (value, self->editable);
        break;
    case PROP_COMMAND:
        g_value_set_string (value, self->command);
        break;
    case PROP_IS_VALUE_DEFAULT:
        g_value_set_boolean (value, cc_keyboard_item_is_value_default (self));
        break;
    case PROP_KEY_COMBOS:
        g_value_set_pointer (value, self->key_combos);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
cc_keyboard_item_class_init (CcKeyboardItemClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->get_property = cc_keyboard_item_get_property;
    object_class->set_property = cc_keyboard_item_set_property;
    object_class->finalize = cc_keyboard_item_finalize;

    properties[PROP_DESCRIPTION] = g_param_spec_string ("description", NULL, NULL, NULL, G_PARAM_READWRITE);

    properties[PROP_EDITABLE] = g_param_spec_boolean ("editable", NULL, NULL, FALSE, G_PARAM_READABLE);

    properties[PROP_TYPE] =
        g_param_spec_int ("type", NULL, NULL, CC_KEYBOARD_ITEM_TYPE_NONE, CC_KEYBOARD_ITEM_TYPE_GLOBAL_SHORTCUT,
                          CC_KEYBOARD_ITEM_TYPE_NONE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE);

    properties[PROP_COMMAND] = g_param_spec_string ("command", NULL, NULL, NULL, G_PARAM_READWRITE);

    properties[PROP_IS_VALUE_DEFAULT] = g_param_spec_boolean ("is-value-default", NULL, NULL, TRUE, G_PARAM_READABLE);

    properties[PROP_KEY_COMBOS] = g_param_spec_pointer ("key-combos", NULL, NULL, G_PARAM_READABLE);

    g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
cc_keyboard_item_init (CcKeyboardItem *item)
{
}

static void
cc_keyboard_item_finalize (GObject *object)
{
    CcKeyboardItem *item;

    g_return_if_fail (object != NULL);
    g_return_if_fail (CC_IS_KEYBOARD_ITEM (object));

    item = CC_KEYBOARD_ITEM (object);

    if (item->settings != NULL)
        g_object_unref (item->settings);

    /* Free memory */
    g_free (item->gsettings_path);
    g_free (item->description);
    g_free (item->command);
    g_free (item->schema);
    g_free (item->key);
    g_list_free_full (item->key_combos, g_free);
    g_list_free_full (item->default_combos, g_free);

    G_OBJECT_CLASS (cc_keyboard_item_parent_class)->finalize (object);
}

CcKeyboardItem *
cc_keyboard_item_new (CcKeyboardItemType type)
{
    GObject *object;

    object = g_object_new (CC_TYPE_KEYBOARD_ITEM, "type", type, NULL);

    return CC_KEYBOARD_ITEM (object);
}

/* Copied from cc-keyboard-shortcut-dialog.c */
static gboolean
strv_contains_prefix_or_match (char **strv, const char *prefix)
{
    const struct {
        const gchar *key;
        const gchar *untranslated;
        const gchar *synonym;
    } key_aliases[] = {
        { "ctrl", "Ctrl", "ctrl" },   { "win", "Super", "super" }, { "option", NULL, "alt" },
        { "command", NULL, "super" }, { "apple", NULL, "super" },
    };

    for (guint i = 0; strv[i]; i++) {
        if (g_str_has_prefix (strv[i], prefix))
            return TRUE;
    }

    for (guint i = 0; i < G_N_ELEMENTS (key_aliases); i++) {
        g_autofree char *alias = NULL;
        const char *synonym;

        if (!g_str_has_prefix (key_aliases[i].key, prefix))
            continue;

        if (key_aliases[i].untranslated) {
            const char *translated_label;

            /* Steal GTK+'s translation */
            translated_label = g_dpgettext2 ("gtk40", "keyboard label", key_aliases[i].untranslated);
            alias = g_utf8_strdown (translated_label, -1);
        }

        synonym = key_aliases[i].synonym;

        /* If a translation or synonym of the key is in the accelerator, and we typed
         * the key, also consider that a prefix */
        if ((alias && g_strv_contains ((const char *const *) strv, alias))
            || (synonym && g_strv_contains ((const char *const *) strv, synonym)))
            return TRUE;
    }

    return FALSE;
}

/* Copied from cc-keyboard-shortcut-dialog.c */
static gboolean
search_match_shortcut (CcKeyboardItem *item, const char *search)
{
    g_auto(GStrv) shortcut_tokens = NULL, search_tokens = NULL;
    g_autofree char *normalized_accel = NULL;
    g_autofree char *accel = NULL;
    GList *key_combos;
    CcKeyCombo *combo;
    gboolean match = TRUE;

    key_combos = cc_keyboard_item_get_key_combos (item);
    for (GList *l = key_combos; l != NULL; l = l->next) {
        combo = l->data;

        if (is_empty_binding (combo))
            continue;

        accel = convert_keysym_state_to_string (combo);
        normalized_accel = cc_util_normalize_casefold_and_unaccent (accel);

        shortcut_tokens = g_strsplit_set (normalized_accel, SHORTCUT_DELIMITERS, -1);
        search_tokens = g_strsplit_set (search, SHORTCUT_DELIMITERS, -1);

        for (guint i = 0; search_tokens[i]; i++) {
            match = match && strv_contains_prefix_or_match (shortcut_tokens, search_tokens[i]);

            if (!match)
                break;
        }

        if (match)
            return TRUE;
    }

    return FALSE;
}

gboolean
cc_keyboard_item_matches_string (CcKeyboardItem *self, GStrv search_terms)
{
    g_autofree char *name = NULL;

    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (self), FALSE);

    if (!search_terms || !*search_terms || !self->description)
        return TRUE;

    name = cc_util_normalize_casefold_and_unaccent (self->description);

    for (guint i = 0; search_terms[i]; i++) {
        gboolean match;

        match = strstr (name, search_terms[i]) || search_match_shortcut (self, search_terms[i]);

        if (!match)
            return FALSE;
    }

    return TRUE;
}

static guint *
get_above_tab_keysyms (void)
{
    guint keycode = 0x29 /* KEY_GRAVE */ + 8;
    g_autofree guint *keyvals = NULL;
    GArray *keysyms;
    int n_entries, i, j;

    keysyms = g_array_new (TRUE, FALSE, sizeof (guint));

    if (!gdk_display_map_keycode (gdk_display_get_default (), keycode, NULL, &keyvals, &n_entries))
        goto out;

    for (i = 0; i < n_entries; i++) {
        gboolean found = FALSE;

        for (j = 0; j < keysyms->len; j++)
            if (g_array_index (keysyms, guint, j) == keyvals[i]) {
                found = TRUE;
                break;
            }

        if (!found)
            g_array_append_val (keysyms, keyvals[i]);
    }

out:
    return (guint *) (gpointer) g_array_free (keysyms, FALSE);
}

/*
 * translate_above_tab:
 *
 * @original_bindings: A list of accelerator strings
 * @new_bindings: (out): Translated bindings if translation is needed
 *
 * Translate accelerator strings that contain the Above_Tab fake keysym
 * used by mutter to strings that use the real keysyms that correspond
 * to the key that is located physically above the tab key.
 *
 * Returns: %TRUE if strings were translated, %FALSE if @original_bindings
 *   can be used unmodified
 */
static gboolean
translate_above_tab (char **original_bindings, char ***new_bindings)
{
    GPtrArray *replaced_bindings;
    g_autofree guint *above_tab_keysyms = NULL;
    gboolean needs_translation = FALSE;
    char **str;

    for (str = original_bindings; *str && !needs_translation; str++)
        needs_translation = strstr (*str, "Above_Tab") != NULL;

    if (!needs_translation)
        return FALSE;

    above_tab_keysyms = get_above_tab_keysyms ();

    replaced_bindings = g_ptr_array_new ();

    for (str = original_bindings; *str; str++) {
        if (strstr (*str, "Above_Tab") == NULL) {
            g_ptr_array_add (replaced_bindings, g_strdup (*str));
        } else {
            g_auto(GStrv) split_str = g_strsplit (*str, "Above_Tab", -1);
            int i;

            for (i = 0; above_tab_keysyms[i]; i++) {
                g_autofree char *sym = NULL;

                sym = gtk_accelerator_name (above_tab_keysyms[i], 0);
                g_ptr_array_add (replaced_bindings, g_strjoinv (sym, split_str));
            }
        }
        g_ptr_array_add (replaced_bindings, NULL);
    }

    *new_bindings = (char **) g_ptr_array_free (replaced_bindings, FALSE);
    return TRUE;
}

static GList *
variant_get_key_combos (GVariant *variant)
{
    GList *combos = NULL;
    char **translated_bindings, **str;
    g_auto(GStrv) bindings = NULL;

    if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING)) {
        bindings = g_malloc0_n (2, sizeof (char *));
        if (g_strcmp0 (g_variant_get_string (variant, NULL), "") != 0)
            bindings[0] = g_variant_dup_string (variant, NULL);
    } else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY)) {
        bindings = g_variant_dup_strv (variant, NULL);
    }

    if (translate_above_tab (bindings, &translated_bindings)) {
        g_strfreev (bindings);
        bindings = translated_bindings;
    }

    for (str = bindings; *str; str++) {
        g_autofree CcKeyCombo *combo = g_new (CcKeyCombo, 1);

        binding_from_string (*str, combo);

        if (combo->keyval != 0 || combo->keycode != 0 || combo->mask != 0)
            combos = g_list_prepend (combos, g_steal_pointer (&combo));
    }

    return g_list_reverse (combos);
}

static GList *
settings_get_key_combos (GSettings *settings, const char *key, gboolean use_default)
{
    GList *key_combos;
    g_autoptr(GVariant) variant = NULL;

    if (use_default)
        variant = g_settings_get_default_value (settings, key);
    else
        variant = g_settings_get_value (settings, key);
    key_combos = variant_get_key_combos (variant);

    return key_combos;
}

static void
binding_changed_notify (CcKeyboardItem *item)
{
    g_object_notify_by_pspec (G_OBJECT (item), properties[PROP_KEY_COMBOS]);
    g_object_notify_by_pspec (G_OBJECT (item), properties[PROP_IS_VALUE_DEFAULT]);
    if (item->reverse_item)
        g_object_notify_by_pspec (G_OBJECT (item->reverse_item), properties[PROP_IS_VALUE_DEFAULT]);
}

static void
binding_changed (CcKeyboardItem *item, const char *key)
{
    g_list_free_full (item->key_combos, g_free);
    item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);

    item->editable = g_settings_is_writable (item->settings, item->key);

    binding_changed_notify (item);
}

static gchar *
combo_get_accelerator (CcKeyCombo *combo)
{
    return gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask);
}

GVariant *
cc_keyboard_item_store_to_global_shortcuts_variant (CcKeyboardItem *item)
{
    g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}"));

    if (item->type != CC_KEYBOARD_ITEM_TYPE_GLOBAL_SHORTCUT)
        return NULL;

    if (item->key_combos) {
        g_auto(GVariantBuilder) shortcuts = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("as"));
        GList *l;

        for (l = item->key_combos; l; l = l->next) {
            g_autofree char *accel = NULL;

            accel = combo_get_accelerator (l->data);
            g_variant_builder_add (&shortcuts, "s", accel);
        }

        g_variant_builder_add (&builder, "{sv}", "shortcuts", g_variant_builder_end (&shortcuts));
    }

    if (item->description) {
        g_variant_builder_add (&builder, "{sv}", "description", g_variant_new_string (item->description));
    }

    return g_variant_builder_end (&builder);
}

/* Fills in a preliminary (?) `item` with data from a permanent source.
 * Uses an existing gsettings preference if available.
 */
gboolean
cc_keyboard_item_load_from_global_shortcuts (CcKeyboardItem *item, const char *shortcut_id, GVariant *properties)
{
    GVariantDict dict;
    GVariant *shortcuts;
    g_autoptr(GSettings) settings = NULL;

    g_variant_dict_init (&dict, properties);

    item->type = CC_KEYBOARD_ITEM_TYPE_GLOBAL_SHORTCUT;
    item->key = g_strdup (shortcut_id);
    item->editable = TRUE;
    g_variant_dict_lookup (&dict, "description", "s", &item->description);
    item->desc_editable = FALSE;
    item->can_set_multiple = TRUE;

    shortcuts = g_variant_dict_lookup_value (&dict, "shortcuts", NULL);
    if (shortcuts)
        item->key_combos = variant_get_key_combos (shortcuts);

    return TRUE;
}

gboolean
cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item, const char *path, gboolean reset)
{
    g_autoptr(GVariant) variant = NULL;

    item->schema = g_strdup (CUSTOM_KEYS_SCHEMA);
    item->gsettings_path = g_strdup (path);
    item->key = g_strdup ("binding");
    item->settings = g_settings_new_with_path (item->schema, path);
    item->editable = g_settings_is_writable (item->settings, item->key);
    item->desc_editable = g_settings_is_writable (item->settings, "name");
    item->cmd_editable = g_settings_is_writable (item->settings, "command");

    variant = g_settings_get_value (item->settings, item->key);
    item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY);

    if (reset) {
        g_settings_reset (item->settings, "name");
        g_settings_reset (item->settings, "command");
        g_settings_reset (item->settings, "binding");
    }

    g_settings_bind (item->settings, "name", G_OBJECT (item), "description", G_SETTINGS_BIND_DEFAULT);
    g_settings_bind (item->settings, "command", G_OBJECT (item), "command", G_SETTINGS_BIND_DEFAULT);

    g_list_free_full (item->key_combos, g_free);
    item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);

    g_signal_connect_object (G_OBJECT (item->settings), "changed::binding", G_CALLBACK (binding_changed), item,
                             G_CONNECT_SWAPPED);

    return TRUE;
}

gboolean
cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item, const char *description, const char *schema,
                                      const char *key)
{
    g_autofree char *signal_name = NULL;
    g_autoptr(GVariant) variant = NULL;

    item->schema = g_strdup (schema);
    item->key = g_strdup (key);
    item->description = g_strdup (description);

    item->settings = g_settings_new (item->schema);
    item->editable = g_settings_is_writable (item->settings, item->key);

    g_list_free_full (item->key_combos, g_free);
    item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);

    g_list_free_full (item->default_combos, g_free);
    item->default_combos = settings_get_key_combos (item->settings, item->key, TRUE);

    variant = g_settings_get_value (item->settings, item->key);
    item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY);

    signal_name = g_strdup_printf ("changed::%s", item->key);
    g_signal_connect_object (G_OBJECT (item->settings), signal_name, G_CALLBACK (binding_changed), item,
                             G_CONNECT_SWAPPED);

    return TRUE;
}

gboolean
cc_keyboard_item_equal (CcKeyboardItem *a, CcKeyboardItem *b)
{
    if (a->type != b->type)
        return FALSE;
    switch (a->type) {
    case CC_KEYBOARD_ITEM_TYPE_GLOBAL_SHORTCUT:
        return g_str_equal (a->key, b->key);
    case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH:
        return g_str_equal (a->gsettings_path, b->gsettings_path);
    case CC_KEYBOARD_ITEM_TYPE_GSETTINGS:
        return (g_str_equal (a->schema, b->schema) && g_str_equal (a->key, b->key));
    default:
        g_assert_not_reached ();
    }
}

void
cc_keyboard_item_add_reverse_item (CcKeyboardItem *item, CcKeyboardItem *reverse_item, gboolean is_reversed)
{
    g_return_if_fail (item->key != NULL);

    item->reverse_item = reverse_item;
    if (reverse_item->reverse_item == NULL) {
        reverse_item->reverse_item = item;
        reverse_item->is_reversed = !is_reversed;
    } else
        g_warn_if_fail (reverse_item->is_reversed == !!is_reversed);

    item->is_reversed = !!is_reversed;
}

CcKeyboardItem *
cc_keyboard_item_get_reverse_item (CcKeyboardItem *item)
{
    return item->reverse_item;
}

void
cc_keyboard_item_set_hidden (CcKeyboardItem *item, gboolean hidden)
{
    item->hidden = !!hidden;
}

gboolean
cc_keyboard_item_is_hidden (CcKeyboardItem *item)
{
    return item->hidden;
}

/**
 * cc_keyboard_item_is_value_default:
 * @self: a #CcKeyboardItem
 *
 * Retrieves whether the shortcut is the default value or not.
 *
 * Returns: %TRUE if the shortcut is the default value, %FALSE otherwise.
 */
gboolean
cc_keyboard_item_is_value_default (CcKeyboardItem *self)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (self), FALSE);

    /*
     * When the shortcut is custom, we don't treat it as modified
     * since we don't know what would be its default value.
     */
    if (self->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
        return TRUE;

    return combos_equal (self->default_combos, self->key_combos);
}

/**
 * cc_keyboard_item_reset:
 * @self: a #CcKeyboardItem
 *
 * Reset the keyboard binding to the default value.
 */
void
cc_keyboard_item_reset (CcKeyboardItem *self)
{
    CcKeyboardItem *reverse;

    g_return_if_fail (CC_IS_KEYBOARD_ITEM (self));

    reverse = self->reverse_item;

    if (self->settings) {
        g_settings_reset (self->settings, self->key);
    } else {
        g_list_free_full (self->key_combos, g_free);
        self->key_combos = NULL;
        binding_changed_notify (self);
    }

    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_IS_VALUE_DEFAULT]);

    /* Also reset the reverse item */
    if (reverse) {
        g_settings_reset (reverse->settings, reverse->key);
        g_object_notify_by_pspec (G_OBJECT (reverse), properties[PROP_IS_VALUE_DEFAULT]);
    }
}

GList *
cc_keyboard_item_get_key_combos (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
    return item->key_combos;
}

GList *
cc_keyboard_item_get_default_combos (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
    return item->default_combos;
}

CcKeyCombo
cc_keyboard_item_get_primary_combo (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), EMPTY_COMBO);
    return (item->key_combos) ? *((CcKeyCombo *) item->key_combos->data) : EMPTY_COMBO;
}

const gchar *
cc_keyboard_item_get_key (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
    return item->key;
}

CcKeyboardItemType
cc_keyboard_item_get_item_type (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), CC_KEYBOARD_ITEM_TYPE_NONE);
    return item->type;
}

const gchar *
cc_keyboard_item_get_gsettings_path (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
    return item->gsettings_path;
}

GSettings *
cc_keyboard_item_get_settings (CcKeyboardItem *item)
{
    g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
    return item->settings;
}

gboolean
cc_keyboard_item_can_set_multiple (CcKeyboardItem *item)
{
    return item->can_set_multiple;
}

static void
cc_keyboard_item_add_key_combo_inner (CcKeyboardItem *self, CcKeyCombo *combo)
{
    g_auto(GStrv) strv = NULL;
    int i;

    if (!self->can_set_multiple) {
        /* In-memory keyboard items currently always support multiple,
         * so we don't bother special-casing when they don't.
         */
        g_assert (self->settings);
        g_settings_set_string (self->settings, self->key, combo_get_accelerator (combo));
    } else {
        strv = g_new0 (gchar *, g_list_length (self->key_combos) + 2);

        i = 0;
        for (GList *l = self->key_combos; l != NULL; l = l->next, i++) {
            /* This combo is already in the list */
            if (combo_equal (l->data, combo))
                return;
            strv[i] = combo_get_accelerator (l->data);
        }
        strv[i] = combo_get_accelerator (combo);

        if (self->settings) {
            g_settings_set_strv (self->settings, self->key, (const gchar **) strv);
        } else /* In-memory keyboard item */
        {
            CcKeyCombo *new_combo;
            new_combo = g_new0 (CcKeyCombo, 1);
            *new_combo = *combo;
            self->key_combos = g_list_append (self->key_combos, new_combo);
        }
    }

    if (self->settings)
        binding_changed (self, self->key);
    else
        binding_changed_notify (self);
}

void
cc_keyboard_item_add_key_combo (CcKeyboardItem *self, CcKeyCombo *combo)
{
    CcKeyCombo reverse_combo;

    if (self->reverse_item) {
        reverse_combo.keyval = combo->keyval;
        reverse_combo.keycode = combo->keycode;
        reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK;
        cc_keyboard_item_add_key_combo_inner (self->reverse_item, &reverse_combo);
    }

    cc_keyboard_item_add_key_combo_inner (self, combo);
}

static void
cc_keyboard_item_remove_key_combo_inner (CcKeyboardItem *self, CcKeyCombo *combo)
{
    g_auto(GStrv) strv = NULL;
    gboolean found;
    int i;

    strv = g_new0 (gchar *, g_list_length (self->key_combos) + 1);

    found = FALSE;
    i = 0;
    for (GList *l = self->key_combos; l != NULL; l = l->next, i++) {
        if (combo_equal (l->data, combo)) {
            i--;
            found = TRUE;
        } else {
            strv[i] = combo_get_accelerator (l->data);
        }
    }

    if (found) {
        if (self->can_set_multiple)
            g_settings_set_strv (self->settings, self->key, (const gchar **) strv);
        else
            g_settings_set_string (self->settings, self->key, "");
    }

    binding_changed (self, self->key);
}

void
cc_keyboard_item_remove_key_combo (CcKeyboardItem *self, CcKeyCombo *combo)
{
    CcKeyCombo reverse_combo;

    if (self->reverse_item) {
        reverse_combo.keyval = combo->keyval;
        reverse_combo.keycode = combo->keycode;
        reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK;
        cc_keyboard_item_remove_key_combo_inner (self->reverse_item, &reverse_combo);
    }

    cc_keyboard_item_remove_key_combo_inner (self, combo);
}

void
cc_keyboard_item_disable (CcKeyboardItem *self)
{
    if (!self->settings) {
        g_list_free_full (self->key_combos, g_free);
        self->key_combos = NULL;
        binding_changed_notify (self);
        return;
    }

    if (!self->can_set_multiple) {
        g_settings_set_string (self->settings, self->key, "");
        if (self->reverse_item)
            g_settings_set_string (self->reverse_item->settings, self->reverse_item->key, "");
    } else {
        g_settings_set_strv (self->settings, self->key, NULL);
        if (self->reverse_item)
            g_settings_set_strv (self->reverse_item->settings, self->reverse_item->key, NULL);
    }

    binding_changed (self, self->key);
}
