/* -*- 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 <stdlib.h>
#include <stdio.h>

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

#include "cc-util.h"
#include "keyboard-shortcuts.h"
#include "cc-keyboard-item.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
};

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_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_free (item->description);
  item->description = g_strdup (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_free (item->command);
  item->command = g_strdup (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;

  g_object_class_install_property (object_class,
                                   PROP_DESCRIPTION,
                                   g_param_spec_string ("description",
                                                        "description",
                                                        "description",
                                                        NULL,
                                                        G_PARAM_READWRITE));

  g_object_class_install_property (object_class,
                                   PROP_EDITABLE,
                                   g_param_spec_boolean ("editable",
                                                         NULL,
                                                         NULL,
                                                         FALSE,
                                                         G_PARAM_READABLE));

  g_object_class_install_property (object_class,
                                   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));

  g_object_class_install_property (object_class,
                                   PROP_COMMAND,
                                   g_param_spec_string ("command",
                                                        "command",
                                                        "command",
                                                        NULL,
                                                        G_PARAM_READWRITE));

  g_object_class_install_property (object_class,
                                   PROP_IS_VALUE_DEFAULT,
                                   g_param_spec_boolean ("is-value-default",
                                                         "is value default",
                                                         "is value default",
                                                         TRUE,
                                                         G_PARAM_READABLE));

  g_object_class_install_property (object_class,
                                   PROP_KEY_COMBOS,
                                   g_param_spec_pointer ("key-combos",
                                                         "key combos",
                                                         "key combos",
                                                         G_PARAM_READABLE));
}

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 *)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 (G_OBJECT (item), "key-combos");
  g_object_notify (G_OBJECT (item), "is-value-default");
  if (item->reverse_item)
    g_object_notify (G_OBJECT (item->reverse_item), "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 (G_OBJECT (self), "is-value-default");

  /* Also reset the reverse item */
  if (reverse)
    {
      g_settings_reset (reverse->settings, reverse->key);
      g_object_notify (G_OBJECT (reverse), "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);
}
