#include <config.h>

#include <string.h>
#include <X11/keysym.h>
#include <glib/gi18n.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include "gnome-settings-daemon.h"
#include "gnome-settings-keybindings.h"
#include "eggaccelerators.h"

/* we exclude shift, GDK_CONTROL_MASK and GDK_MOD1_MASK since we know what 
   these modifiers mean 
   these are the mods whose combinations are bound by the keygrabbing code */
#define IGNORED_MODS (0x2000 /*Xkb modifier*/ | GDK_LOCK_MASK  | \
        GDK_MOD2_MASK | GDK_MOD3_MASK | GDK_MOD4_MASK | GDK_MOD5_MASK) 
/* these are the ones we actually use for global keys, we always only check
 * for these set */
#define USED_MODS (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)

#define GCONF_BINDING_DIR "/desktop/gnome/keybindings"

typedef struct {
  guint keysym;
  guint state;
  guint keycode;
} Key;

typedef struct {
  char *binding_str;
  char *action;
  char *gconf_key;
  Key   key;
  Key   previous_key;
} Binding;
  
static GSList *binding_list = NULL;
static GSList *screens = NULL;

static GSList *
get_screens_list (void)
{
  GdkDisplay *display = gdk_display_get_default();
  GSList *list = NULL;
  int i;

  if (gdk_display_get_n_screens (display) == 1) {
    list = g_slist_append (list, gdk_screen_get_default ());
  } else {
    for (i = 0; i < gdk_display_get_n_screens (display); i++) {
      GdkScreen *screen;

      screen = gdk_display_get_screen (display, i);
      if (screen != NULL) {
        list = g_slist_append (list, screen);
      }
    }
  }

  return list;
}

extern char **environ;

static char *
screen_exec_display_string (GdkScreen *screen)
{
  GString    *str;
  const char *old_display;
  char       *retval;
  char       *p;

  g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL);

  old_display = gdk_display_get_name (gdk_screen_get_display (screen));

  str = g_string_new ("DISPLAY=");
  g_string_append (str, old_display);

  p = strrchr (str->str, '.');
  if (p && p >  strchr (str->str, ':'))
          g_string_truncate (str, p - str->str);

  g_string_append_printf (str, ".%d", gdk_screen_get_number (screen));

  retval = str->str;

  g_string_free (str, FALSE);

  return retval;
}

/**
 * get_exec_environment:
 *
 * Description: Modifies the current program environment to
 * ensure that $DISPLAY is set such that a launched application
 * inheriting this environment would appear on screen.
 *
 * Returns: a newly-allocated %NULL-terminated array of strings or
 * %NULL on error. Use g_strfreev() to free it.
 *
 * mainly ripped from egg_screen_exec_display_string in
 * gnome-panel/egg-screen-exec.c
 **/
static char **
get_exec_environment (XEvent *xevent)
{
  char **retval = NULL;
  int    i;
  int    display_index = -1;

  GdkScreen  *screen = NULL;

  GdkWindow *window = gdk_xid_table_lookup (xevent->xkey.root);

  if (window)
    screen = gdk_drawable_get_screen (GDK_DRAWABLE (window));

  g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL);

  for (i = 0; environ [i]; i++)
    if (!strncmp (environ [i], "DISPLAY", 7))
      display_index = i;

  if (display_index == -1)
    display_index = i++;

  retval = g_new (char *, i + 1);

  for (i = 0; environ [i]; i++)
    if (i == display_index)
      retval [i] = screen_exec_display_string (screen);
    else
      retval [i] = g_strdup (environ [i]);

  retval [i] = NULL;

  return retval;
}


static gint 
compare_bindings (gconstpointer a, gconstpointer b)
{
  Binding *key_a = (Binding*) a;
  char *key_b = (char*) b;

  return strcmp (key_b, key_a->gconf_key);
}

static gboolean
parse_binding (Binding *binding)
{
  g_return_val_if_fail (binding != NULL, FALSE);

  binding->key.keysym = 0;
  binding->key.state = 0;

  if (binding->binding_str == NULL ||
      binding->binding_str[0] == '\0' ||
      strcmp (binding->binding_str, "Disabled") == 0)
          return FALSE;

  if (egg_accelerator_parse_virtual (binding->binding_str, &binding->key.keysym, &binding->key.keycode, &binding->key.state) == FALSE)
          return FALSE;

  return TRUE;
}

static gboolean 
bindings_get_entry (char *subdir)
{
  GConfValue *value;
  Binding *new_binding;
  GSList *tmp_elem = NULL, *list = NULL, *li;
  char *gconf_key;
  char *action = NULL;
  char *key = NULL;
  GConfClient *client = gnome_settings_daemon_get_conf_client ();
  
  g_return_val_if_fail (subdir != NULL, FALSE);
  
  /* value = gconf_entry_get_value (entry); */
  gconf_key = g_path_get_basename (subdir);

  if (!gconf_key)
    return FALSE;

  /* Get entries for this binding */
  list = gconf_client_all_entries (client, subdir, NULL);

  for (li = list; li != NULL; li = li->next)
    {
      GConfEntry *entry = li->data;
      char *key_name = g_path_get_basename (gconf_entry_get_key (entry));
      if (strcmp (key_name, "action") == 0)
	{
	  if (!action)
	    {
	      value = gconf_entry_get_value (entry);
	      if (value->type != GCONF_VALUE_STRING)
		return FALSE;
	      action = g_strdup (gconf_value_get_string (value));
	    }
	  else
	    g_warning (_("Key Binding (%s) has its action defined multiple times\n"),
		       gconf_key);
	}
      if (strcmp (key_name, "binding") == 0)
	{
	  if (!key)
	    {
	      value = gconf_entry_get_value (entry);
	      if (value->type != GCONF_VALUE_STRING)
		return FALSE;
	      key = g_strdup (gconf_value_get_string (value));
	    }
	  else
	    g_warning (_("Key Binding (%s) has its binding defined multiple times\n"),
		       gconf_key);
	}
    }
  if (!action || !key)
    { 
      g_warning (_("Key Binding (%s) is incomplete\n"), gconf_key);
      return FALSE;
    }
    
  tmp_elem = g_slist_find_custom (binding_list, gconf_key,
				  compare_bindings);

  if (!tmp_elem)
    new_binding = g_new0 (Binding, 1);
  else
    {
      new_binding = (Binding*) tmp_elem->data;
      g_free (new_binding->binding_str);
      g_free (new_binding->action);
    }
  
  new_binding->binding_str = key;
  new_binding->action = action;
  new_binding->gconf_key = gconf_key;

  new_binding->previous_key.keysym = new_binding->key.keysym;
  new_binding->previous_key.state = new_binding->key.state;
  new_binding->previous_key.keycode = new_binding->key.keycode;

  if (parse_binding (new_binding))
      binding_list = g_slist_append (binding_list, new_binding);
  else
    {
      g_warning (_("Key Binding (%s) is invalid\n"), gconf_key);
      g_free (new_binding->binding_str);
      g_free (new_binding->action);
      return FALSE;
    }
  return TRUE;
}

static gboolean 
key_already_used (Binding *binding)
{
  GSList *li;
  
  for (li = binding_list; li != NULL; li = li->next)
    {
      Binding *tmp_binding =  (Binding*) li->data;

      if (tmp_binding != binding &&  tmp_binding->key.keycode == binding->key.keycode &&
	  tmp_binding->key.state == binding->key.state)
	return TRUE;
    }
  return FALSE;
}

static void
grab_key (GdkWindow *root, Key *key, int result, gboolean grab)
{
  gdk_error_trap_push ();
  if (grab)
    XGrabKey (GDK_DISPLAY(), key->keycode, (result | key->state),
              GDK_WINDOW_XID (root), True, GrabModeAsync, GrabModeAsync);
  else
    XUngrabKey(GDK_DISPLAY(), key->keycode, (result | key->state),
	       GDK_WINDOW_XID (root));
  gdk_flush ();
  if (gdk_error_trap_pop ()) {
    g_warning (_("It seems that another application already has"
                 " access to key '%u'."), key->keycode);
  }
}

/* inspired from all_combinations from gnome-panel/gnome-panel/global-keys.c */
#define N_BITS 32
static void
do_grab (gboolean grab,
	 Key *key)
{
	int indexes[N_BITS];/*indexes of bits we need to flip*/
	int i, bit, bits_set_cnt;
	int uppervalue;
	guint mask_to_traverse = IGNORED_MODS & ~ key->state;
	  
	bit = 0;
	for (i = 0; i < N_BITS; i++) {
		if (mask_to_traverse & (1<<i))
			indexes[bit++]=i;
	}

	bits_set_cnt = bit;

	uppervalue = 1<<bits_set_cnt;
	for (i = 0; i < uppervalue; i++) {
		GSList *l;
		int j, result = 0;

		for (j = 0; j < bits_set_cnt; j++) {
			if (i & (1<<j))
				result |= (1<<indexes[j]);
		}

		for (l = screens; l ; l = l->next) {
                  GdkScreen *screen = l->data;
                  grab_key (gdk_screen_get_root_window (screen), key, result,
			    grab);
		}
	}
}

static void
binding_register_keys (void)
{
  GSList *li;
  
  gdk_error_trap_push();

  /* Now check for changes and grab new key if not already used */
  for (li = binding_list ; li != NULL; li = li->next)
    {
      Binding *binding = (Binding *) li->data;
      
      if (binding->previous_key.keycode != binding->key.keycode || 
	  binding->previous_key.state != binding->key.state)
        {
          /* Ungrab key if it changed and not clashing with previously set binding */
          if (!key_already_used (binding))
            {
              if (binding->previous_key.keycode)
		do_grab (FALSE, &binding->previous_key);
	      do_grab (TRUE, &binding->key);

	      binding->previous_key.keysym = binding->key.keysym;
	      binding->previous_key.state = binding->key.state;
	      binding->previous_key.keycode = binding->key.keycode;
            }
          else
            g_warning (_("Key Binding (%s) is already in use\n"), binding->binding_str);
        }
    }
  gdk_flush ();
  gdk_error_trap_pop();
}

static void
bindings_callback (GConfEntry *entry)
{
  /* ensure we get binding dir not a sub component */
  gchar** key_elems = g_strsplit (gconf_entry_get_key (entry), "/", 15);
  gchar* binding_entry  = g_strdup_printf ("/%s/%s/%s/%s", key_elems[1], 
					   key_elems[2], key_elems[3], 
					   key_elems[4]);					 
  g_strfreev (key_elems);
  
  bindings_get_entry (binding_entry);
  g_free (binding_entry);
  
  binding_register_keys ();
}

static GdkFilterReturn
keybindings_filter (GdkXEvent *gdk_xevent,
		    GdkEvent *event,
		    gpointer data)
{
  XEvent *xevent = (XEvent *)gdk_xevent;
  guint keycode, state;
  GSList *li;

  if(xevent->type != KeyPress)
          return GDK_FILTER_CONTINUE;

  keycode = xevent->xkey.keycode;
  state = xevent->xkey.state;
  
  for (li = binding_list; li != NULL; li = li->next)
    {
      Binding *binding = (Binding*) li->data;
      
      if (keycode == binding->key.keycode &&
          (state & USED_MODS) == binding->key.state)
        {
          GError* error = NULL;
	  gboolean retval;
	  gchar **argv = NULL;
	  gchar **envp = NULL;

	  g_return_val_if_fail (binding->action != NULL, GDK_FILTER_CONTINUE);

	  if (!g_shell_parse_argv (binding->action,
				   NULL, &argv,
				   &error))
	    return GDK_FILTER_CONTINUE;

	  envp = get_exec_environment (xevent);

	  
	  retval = g_spawn_async (NULL,
				  argv,
				  envp,
				  G_SPAWN_SEARCH_PATH,
				  NULL,
				  NULL,
				  NULL,
				  &error);
	  g_strfreev (argv);
	  g_strfreev (envp);

          if (!retval)
	    {
	      GtkWidget *dialog = gtk_message_dialog_new (NULL, 0, GTK_MESSAGE_WARNING,
							  GTK_BUTTONS_CLOSE,
							  _("Error while trying to run (%s)\n"\
							  "which is linked to the key (%s)"),
							  binding->action,
							  binding->binding_str);
	      	g_signal_connect (dialog, "response",
				  G_CALLBACK (gtk_widget_destroy),
				  NULL);
		gtk_widget_show (dialog);
	    }
          return GDK_FILTER_REMOVE;
        }
    }
  return GDK_FILTER_CONTINUE;
}

void
gnome_settings_keybindings_init (GConfClient *client)
{
  GdkDisplay *dpy = gdk_display_get_default ();
  GdkScreen *screen;
  int screen_num = gdk_display_get_n_screens (dpy);
  int i;

  gnome_settings_daemon_register_callback (GCONF_BINDING_DIR, bindings_callback);
  
  gdk_window_add_filter (gdk_get_default_root_window (),
			 keybindings_filter,
			 NULL);
  for (i = 0; i < screen_num; i++)
    {
      screen = gdk_display_get_screen (dpy, i);
      gdk_window_add_filter (gdk_screen_get_root_window (screen),
			     keybindings_filter, NULL);
    }
}

void
gnome_settings_keybindings_load (GConfClient *client)
{
  GSList *list, *li;

  list = gconf_client_all_dirs (client, GCONF_BINDING_DIR, NULL);
  screens = get_screens_list ();

  for (li = list; li != NULL; li = li->next)
    {
      char *subdir = li->data;
      li->data = NULL;
      bindings_get_entry (subdir);
    }
  binding_register_keys ();
}

