/*  gfocustimer
 *  Copyright (C) 1999 James Cameron, <quozl@us.netrek.org>
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#define XK_MISCELLANY
#include <X11/keysymdef.h>
#include <gdk/gdkkeysyms.h>

#include "callbacks.h"
#include "interface.h"
#include "support.h"
#include "utilities.h"
#include "data.h"
#include "total.h"
#include "entry.h"
#include "charge.h"
#include "preferences.h"
#include "focus.h"
#include "connect.h"
#include "observation.h"
#include "shortcuts.h"

/* main window */
extern struct mw mw;

/* preferences window */
extern struct pw pw;

/* preferences data */
extern struct pd pd;


static void action_charge_start()
{
  gtk_toggle_button_set_active (mw.charging, TRUE);
}


static void action_charge_stop()
{
  gtk_toggle_button_set_active (mw.charging, FALSE);
}


static void action_charge_toggle()
{
  int active = gtk_toggle_button_get_active (mw.charging);
  gtk_toggle_button_set_active (mw.charging, ! active);
}


static void action_focus_start()
{
  gtk_toggle_button_set_active (mw.focus, TRUE);
}


static void action_focus_stop()
{
  gtk_toggle_button_set_active (mw.focus, FALSE);
}


static void action_focus_toggle()
{
  int active = gtk_toggle_button_get_active (mw.focus);
  gtk_toggle_button_set_active (mw.focus, ! active);
}


static void action_charge_above()
{
  if (mw.ed == NULL) return;
  if (mw.row < 1) return;

  gtk_clist_select_row (mw.list, mw.row-1, 0);
}


static void action_charge_below()
{
  if (mw.ed == NULL) return;

  gtk_clist_select_row (mw.list, mw.row+1, 0);
}


static void action_charge_current()
{
  if (mw.ed == NULL) return;
  if (mw.row < 1) return;

  gtk_clist_select_row (mw.list, mw.row, 0);

  /* why would anyone want this?
     in watch focus mode, it binds the current window to the current entry */
}


static struct action
{
  int row;
  void (*handler)(void);
  char *name;
} actions[] = {
  { 0, action_charge_start,   "start charging time" },
  { 0, action_charge_stop,    "stop charging time" },
  { 0, action_charge_toggle,  "toggle charging time" },
  { 0, action_focus_start,    "start focus watch" },
  { 0, action_focus_stop,     "stop focus watch" },
  { 0, action_focus_toggle,   "toggle focus watch" },
  { 0, action_charge_above,   "charge to entry above current entry" },
  { 0, action_charge_below,   "charge to entry below current entry" },
  { 0, action_charge_current, "charge to current entry" },
  { 0, entry_create,          "create new entry" },
  { 0, entry_edit,            "edit current entry" },
  { 0, entry_delete,          "delete current entry" },
  { 0, NULL, NULL }
};


struct shortcut
{
  struct action *action;
  unsigned int state, keyval;
  GList *node;
};


/* list of shortcuts defined */
static GList *shortcuts;

/* currently selected action on the preferences window */
static struct action *selected;

/* action being used to grab a key */
static struct action *grabbing;

/* table of GDK key modifier masks and names */
static struct mask { unsigned int mask; char *name; } masks[] = {
  { GDK_SHIFT_MASK,     "Shift" },
  { GDK_LOCK_MASK,      "Lock" },
  { GDK_CONTROL_MASK,   "Control" },
  { GDK_MOD1_MASK,      "Mod1" },
  { GDK_MOD2_MASK,      "Mod2" },
  { GDK_MOD3_MASK,      "Mod3" },
  { GDK_MOD4_MASK,      "Mod4" },
  { GDK_MOD5_MASK,      "Mod5" },
  { GDK_BUTTON1_MASK,   "Button1" },
  { GDK_BUTTON2_MASK,   "Button2" },
  { GDK_BUTTON3_MASK,   "Button3" },
  { GDK_BUTTON4_MASK,   "Button4" },
  { GDK_BUTTON5_MASK,   "Button5" },
  { GDK_RELEASE_MASK,   "Release" },
  { 0,                  NULL },
};


/* translate a modifier state to a string, which must be freed */
static char *state_to_string (int state)
{
  int i;
  char *new, *string;
  struct mask *mask;

  /* start with an empty string */
  string = g_strdup ("");

  /* encode the modifiers into the string */
  for (i=0;;i++) {
    mask = &masks[i];
    if (mask->name == NULL) break; /* end of list */
    if (state & mask->mask) {
      new = g_strconcat (string, mask->name, "+", NULL);
      g_free (string);
      string = new;
    }
  }

  return string;
}  


/* translate a modifier string into a modifier state mask */
static int string_to_state (char *string)
{
  struct mask *mask;
  int i, state = 0;

  for (i=0;;i++) {
    mask = &masks[i];
    if (mask->name == NULL) break; /* end of list */
    if (strstr (string, mask->name) != NULL) {
      state |= mask->mask;
    }
  }

  return state;
}  


/* translate a key value to string */
static char *keyval_to_string (unsigned int keyval)
{
  char *temporary;

  temporary = XKeysymToString (keyval);
  if (temporary != NULL) {
    return g_strdup(temporary);
  } else {
    return g_strdup("");
  }
}


static unsigned int string_to_keyval (char *string)
{
  return XStringToKeysym (string);
}


/* save the shortcuts defined by the user to the file */
static void shortcut_save ()
{
  char *prefix;
  char *suffix;
  FILE *file;
  int i = 0;

  prefix = file_prefix();
  suffix = "shortcuts";
  file = entry_save_open (prefix, suffix);
  if (file == NULL) {
    g_free (prefix);
    return;
  }

  fprintf (file, "# gfocustimer shortcuts file\n");
  fprintf (file, "# format: action-index=(modifiers,key)\n");

  while (1) {
    struct action *action = &actions[i];
    GList *list;
    if (action->handler == NULL) break;

    fprintf (file, "# %d: %s\n", i, action->name);

    list = g_list_first (shortcuts);
    while (list != NULL) {
      if (list->data != NULL) {
	struct shortcut *shortcut = (struct shortcut *)list->data;

	if (shortcut->action == action) {
	  char *state = state_to_string (shortcut->state);
	  char *keyval = keyval_to_string (shortcut->keyval);
	  fprintf (file, "%d=(%s,%s)\n", i, state, keyval);
	  g_free (state);
	  g_free (keyval);
	}
      }
      list = g_list_next (list);
    }
    i++;
  }

  fclose (file);

  g_free (prefix);
}


static char *f_strconcat (char *one, char *two)
{
  char *new = g_strconcat (one, two, NULL);
  g_free (one);
  g_free (two);
  return new;
}


/* displat a shortcut in the preferences window */
static void shortcut_display (struct shortcut *shortcut)
{
  char *string;

  /* encode the modifiers into the string */
  string = state_to_string (shortcut->state);

  /* append the key value to the string */
  string = f_strconcat (string, keyval_to_string (shortcut->keyval));

  /* move to text field */
  gtk_entry_set_text (GTK_ENTRY(lookup(pw.widget, "shortcut_text")), 
		      string);

  /* set the flag */
  gtk_clist_set_text (GTK_CLIST(lookup(pw.widget, "shortcut_list")),
		      shortcut->action->row, 0, "*");

  g_free (string);
}


int shortcut_error_count;


static int shortcut_errors (Display *display, XErrorEvent *event)
{
  shortcut_error_count++;
  return TRUE;
}


/* create a shortcut for the action being grabbed */
static struct shortcut *shortcut_grabber (struct action *action, 
					  unsigned int state,
					  unsigned int keyval)
{
  struct shortcut *shortcut;
  int (*old)(Display *, XErrorEvent *);
  int i;

  /* catch Bad Access errors expected by shortcuts in use */
  old = XSetErrorHandler (shortcut_errors);
  shortcut_error_count = 0;

  /* grab the shortcut key, preventing other applications from using
     it, and causing the X server to deliver the key to us no matter what
     window has focus */
  for(i=0;i<ScreenCount(gdk_display);i++) {
    XGrabKey (gdk_display, XKeysymToKeycode(gdk_display, keyval),
	      state, RootWindowOfScreen(ScreenOfDisplay(gdk_display,i)),
	      FALSE, GrabModeAsync, GrabModeAsync);
  }
  
  /* force any errors to be reported */
  XSync (gdk_display, FALSE);

  /* cancel our error handler by reinstating the old one */
  (void) XSetErrorHandler (old);

  /* if we could not grab the key, do not create the shortcut */
  if (shortcut_error_count) return NULL;

  /* create the shortcut structure */
  shortcut = g_new(struct shortcut, 1);
  shortcut->action = action;
  shortcut->state  = state;
  shortcut->keyval = keyval;

  /* append the shortcut to the list */
  shortcut->node = list_append (shortcuts, shortcut);

  return shortcut;
}


/* release the key grab for a shortcut being cleared */
static void shortcut_ungrabber (struct shortcut *shortcut)
{
  int i;

  for(i=0;i<ScreenCount(gdk_display);i++) {
    XUngrabKey (gdk_display, XKeysymToKeycode(gdk_display, shortcut->keyval),
		shortcut->state, 
		RootWindowOfScreen(ScreenOfDisplay(gdk_display,i)));
  }
}


/* debug assistance for key press events, if defined write to stdout */
#undef DEBUG


/* handle events to detect grabbed shortcut keys */
static void shortcut_handler (GdkEvent *event, gpointer data)
{
  /* intercept any key press events */
  if (event->type == GDK_KEY_PRESS) {
    GdkEventKey *key = (GdkEventKey *) event;
    GList *list;
    int calls = 0;

#ifdef DEBUG
    printf("state: %d ", key->state);
    printf("keyval: %d ", key->keyval);
#endif

    if (grabbing) {
      struct shortcut *shortcut;

      /* ignore the modifier key events */
      switch (key->keyval) {
      case GDK_Shift_L:
      case GDK_Shift_R:
      case GDK_Control_L:
      case GDK_Control_R:
      case GDK_Caps_Lock:
      case GDK_Shift_Lock:
      case GDK_Meta_L:
      case GDK_Meta_R:
      case GDK_Alt_L:
      case GDK_Alt_R:
      case GDK_Super_L:
      case GDK_Super_R:
      case GDK_Hyper_L:
      case GDK_Hyper_R:
#ifdef DEBUG
	printf ("modifier!\n");
#endif
	return;
      }

      /* define the shortcut */
      shortcut = shortcut_grabber (grabbing, key->state, key->keyval);
      if (shortcut == NULL) {
	gdk_beep ();
	return;
      }

      /* cancel the grab in progress */
      grabbing = NULL;

      /* display the key we got */
      shortcut_display (shortcut);

      /* save it */
      shortcut_save ();

#ifdef DEBUG
      printf ("grabbed!\n");
#endif
      return;
    } /* not grabbing */

    /* scan list of known shortcuts */
    list = g_list_first (shortcuts);
    while (list != NULL) {
      if (list->data != NULL) {
	struct shortcut *shortcut = (struct shortcut *)list->data;

	/* does this event keycode match the shortcut? */
	if (key->keyval == shortcut->keyval) {
	  
	  /* does the modifier mask match too? */
	  if (shortcut->state == AnyModifier ||
	      key->state == shortcut->state) {
	    
	    /* yes, call the handler */
	    (*shortcut->action->handler) ();
	    calls++;
	  }
	}
      }
      list = g_list_next (list);
    }

    /* did we call any handlers?  if so, avoid giving event to GTK+ */
    if (calls) {
#ifdef DEBUG
      printf ("shortcut!\n");
#endif
      return;
    }
    
#ifdef DEBUG
    printf ("gtk!\n");
#endif
  }

  /* hand the other events to GTK+, or it doesn't work */
  (void) gtk_main_do_event (event);
  return;
}


/* initialise the shortcut feature */
void shortcut_initialise ()
{
  /* give the linked list an empty element to play with */
  shortcuts = g_list_alloc();
  shortcuts->data = NULL;

  /* interject our event handler to catch the key press events */
  gdk_event_handler_set (shortcut_handler, NULL, NULL);

  selected = NULL;

  grabbing = NULL;
}


/* when the user selects a shortcut on the preferences window */
void
on_shortcut_list_select_row            (GtkCList        *clist,
                                        gint             row,
                                        gint             column,
                                        GdkEvent        *event,
                                        gpointer         user_data)
{
  struct action *action;
  GList *list;

  /* find the action for this row */
  action = (struct action *) gtk_clist_get_row_data (clist, row);
  if (action == NULL) return;

  /* find any associated shortcut */
  list = g_list_first (shortcuts);
  while (list != NULL) {
    if (list->data != NULL) {
      struct shortcut *shortcut = (struct shortcut *)list->data;
      
      if (shortcut->action == action) {
	shortcut_display (shortcut);
      }
    }
    list = g_list_next (list);
  }

  /* remember selected row */
  selected = action;
}


/* when the user cancels the selection */
void
on_shortcut_list_unselect_row          (GtkCList        *clist,
                                        gint             row,
                                        gint             column,
                                        GdkEvent        *event,
                                        gpointer         user_data)
{
  selected = NULL;
  grabbing = NULL;
  gtk_entry_set_text (GTK_ENTRY(lookup(GTK_WIDGET(clist), "shortcut_text")), 
		      "");
}


/* display the actions in the preferences window */
void shortcut_show (GtkWidget *widget)
{
  char *columns[3];
  int i = 0;

  gtk_clist_set_column_auto_resize (GTK_CLIST(widget), 0, TRUE);

  while (1) {
    struct action *action = &actions[i++];
    GList *list;
    if (action->handler == NULL) break;

    columns[0] = "";

    list = g_list_first (shortcuts);
    while (list != NULL) {
      if (list->data != NULL) {
	struct shortcut *shortcut = (struct shortcut *)list->data;

	/* mark the action as having a shortcut */
	if (shortcut->action == action) {
	  columns[0] = "*";
	}
      }
      list = g_list_next (list);
    }

    columns[1] = action->name;
    columns[2] = NULL;

    action->row = gtk_clist_append (GTK_CLIST(widget), columns);
    gtk_clist_set_row_data (GTK_CLIST(widget), action->row, action);
  }
}


/* when the user clicks the "clear" button */
void
on_shortcut_clear_clicked              (GtkButton       *button,
                                        gpointer         user_data)
{
  struct action *action = selected;
  GList *list;

  /* if no action is yet selected, ignore the button */
  if (action == NULL) return;

  /* find the shortcut, remove from list, free it, */
  list = g_list_first (shortcuts);
  while (list != NULL) {
    if (list->data != NULL) {
      struct shortcut *shortcut = (struct shortcut *)list->data;
      
      if (shortcut->action == action) {

	/* ungrab the key for this shortcut */
	shortcut_ungrabber (shortcut);

	/* remove from list and free */
	g_list_remove (shortcuts, shortcut);
	g_free (shortcut);

	/* clear the text field */
	gtk_entry_set_text (GTK_ENTRY(lookup(GTK_WIDGET(button), 
					     "shortcut_text")), 
			    "");
      }
    }
    list = g_list_next (list);
  }

  /* clear the flag */
  gtk_clist_set_text (GTK_CLIST(lookup(pw.widget, "shortcut_list")),
		      action->row, 0, "");

  /* save the change made to file */
  shortcut_save ();
}


/* when user presses "capture" to define a shortcut key for an action */
void
on_shortcut_capture_clicked            (GtkButton       *button,
                                        gpointer         user_data)
{
  /* clear the existing shortcut */
  on_shortcut_clear_clicked (button, user_data);

  /* tell our key handler we are now grabbing */
  grabbing = selected;
}


/* read the shortcuts from the file and grab them */
void shortcut_load ()
{
  char *prefix;
  char *suffix;
  char *name;
  FILE *file;
  int failures = 0;

  prefix = file_prefix();
  suffix = "shortcuts";
  name = g_strdup_printf ("%s/%s", prefix, suffix);
  g_free (prefix);

  file = fopen (name, "r");
  if (file == NULL) {
    g_free (name);
    return;
  }

  while(1) {
    char *line, buffer[80], *index, *state, *keyval;
    struct shortcut *shortcut;

    line = fgets (buffer, 80, file);
    if (line == NULL) break;

    if (line[0] == '#') continue;	/* ignore comments */

    /* format: index=(state,keyval) */

    index = strtok (line, "=(");
    if (index == NULL) continue;

    state = strtok (NULL, ",");
    if (state == NULL) continue;

    keyval = strtok (NULL, ")\n\r");
    if (keyval == NULL) continue;

    shortcut = shortcut_grabber (&actions[atoi (index)], 
				 string_to_state (state),
				 string_to_keyval (keyval));
    if (shortcut == NULL) failures++;
  }

  fclose (file);

  if (failures) {
    char *text = g_strdup_printf (
"Shortcut grab failed.  While loading the shortcut file %s,\n"
"gfocustimer failed to grab %d shortcut key%s from your X server.\n"
"This usually indicates that another application has already grabbed\n"
"the shortcut keys that you had previously defined.  The most common\n"
"root cause is that you already have a gfocustimer running.  If you\n"
"need to run multiple gfocustimer windows, give each a different HOME\n"
"environment variable.\n",
        name, failures, failures == 1 ? "" : "s");

    message ("gfocustimer shortcut grab failed", text);
    g_free (text);
  }

  g_free (name);
}
