/*****************************************************************************/
/* XKeyWrap */
/*****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Core.h>
#include <X11/Shell.h>

#include "list.h"

#define DEFAULT_FILENAME "key.dat"

/* メニューのフォントのサイズ */
#define FONT_WIDTH   8
#define FONT_HEIGHT 16
#define FONT_OFFSET 14

/* ウインドウのサイズ */
#define WIDTH  (FONT_WIDTH  * 54)
#define HEIGHT (FONT_HEIGHT *  2)

Widget toplevel;
Window childwin;
static Font font;
static GC font_gc; /* 文字表示用の GC */
static XEvent ev;  /* イベント処理用 */
static int focus_menu = 0;
static List key_list = NULL;
static int recording = 0;
static int child = 0;
static char * filename = NULL;
static ListPointer timer_pointer = NULL;
static XtIntervalId timeout_id = 0;

static void select_window();
static void search_parent();
static void clear_sequence();
static void record_sequence();
static void stop_recording();
static void play_sequence();
static void repeat_sequence();
static void save_sequence();
static void quit();

static struct {
  char * name;
  void (*func)();
} menu_struct[] = {
  {"Select", select_window},
  {"Parent", search_parent},
  {"Clear", clear_sequence},
  {"Record", record_sequence},
  {"Stop", stop_recording},
  {"Play", play_sequence},
  {"Repeat", repeat_sequence},
  {"Save", save_sequence},
  {"Quit", quit},
  {NULL, NULL}
};

/*****************************************************************************/
/* さまざまな関数                                                            */
/*****************************************************************************/

/*---------------------------------------------------------------------------*/
/* メッセージの出力(文字列のみ)                                              */
/*---------------------------------------------------------------------------*/

static void output_message(char * message)
{
  static char buffer[100];
  int i, x;
  int len;

  if (message) {
    strncpy(buffer, message, 100);
  }
  XClearWindow(XtDisplay(toplevel), XtWindow(toplevel));

  x = 0;
  for (i = 0; menu_struct[i].name; i++) {
    len = strlen(menu_struct[i].name);
    XDrawString(XtDisplay(toplevel), XtWindow(toplevel), font_gc,
		x * FONT_WIDTH, FONT_OFFSET, menu_struct[i].name, len);
    if (i == focus_menu)
      XDrawRectangle(XtDisplay(toplevel), XtWindow(toplevel), font_gc,
		     x * FONT_WIDTH, 0,
		     len * FONT_WIDTH - 1, FONT_HEIGHT - 1);
    x += len + 1;
  }
  XDrawString(XtDisplay(toplevel), XtWindow(toplevel), font_gc,
	      0, FONT_HEIGHT + FONT_OFFSET,
	      buffer, strlen(buffer));
}

/*---------------------------------------------------------------------------*/
/* メッセージの出力(数値入り)                                                */
/*---------------------------------------------------------------------------*/

static void output_value(char * message, int value)
{
  static char buffer[100];
  sprintf(buffer, message, value);
  output_message(buffer);
}

/*---------------------------------------------------------------------------*/
/* メッセージの出力(文字列入り)                                              */
/*---------------------------------------------------------------------------*/

static void output_string(char * message, char * string)
{
  static char buffer[100];
  sprintf(buffer, message, string);
  output_message(buffer);
}

/*---------------------------------------------------------------------------*/
/* XQueryTree() で得た，子ウインドウの配列を調べる                           */
/*---------------------------------------------------------------------------*/

Window diff_windows(Window * from, unsigned int n_from,
		    Window * to, unsigned int n_to)
{
  int i, j;
  int f;

  for (i = 0; i < n_to; i++) {
    f = 0;
    for (j = 0; j < n_from; j++) {
      if (to[i] == from[j]) {
	f = 1;
	break;
      }
    }
    if (!f) return (to[i]);
  }
  return (0);
}

/*****************************************************************************/
/* メニュー                                                                  */
/*****************************************************************************/

/*---------------------------------------------------------------------------*/
/* マウスポインタが移動したら，メニューのフォーカスも移動する                */
/*---------------------------------------------------------------------------*/

static void pointer_motion(Widget w, XtPointer client_data, XEvent * event,
			   Boolean * dispatch)
{
  int i, x;
  static int old_menu = -1;

  if (event->type == MotionNotify) {
    x = 0;
    for (i = 0; menu_struct[i].name; i++) {
      x += strlen(menu_struct[i].name) + 1;
      if (event->xbutton.x <= x * FONT_WIDTH) {
	focus_menu = i;
	break;
      }
    }
  }
  if (old_menu != focus_menu) output_message(NULL);
  old_menu = focus_menu;
}

/*---------------------------------------------------------------------------*/
/* マウスのボタンをクリックしたら，フォーカスされてるメニューの              */
/* メニューハンドラを実行する                                                */
/*---------------------------------------------------------------------------*/

static void button_press(Widget w, XtPointer client_data, XEvent * event,
			 Boolean * dispatch)
{
  (*(menu_struct[focus_menu].func))();
}

/*===========================================================================*/
/* ハンドラ関数                                                              */
/*===========================================================================*/

/*---------------------------------------------------------------------------*/
/* ウインドウの選択                                                          */
/*---------------------------------------------------------------------------*/

static void select_window()
{
  Window root, child;
  int rx, ry, wx, wy;
  unsigned int mask;

  output_message("Push left button on a window to wrap.");

  do {
    XQueryPointer(XtDisplay(toplevel), DefaultRootWindow(XtDisplay(toplevel)),
		  &root, &child, &rx, &ry, &wx, &wy, &mask);
  } while (mask & Button1Mask);

  do {
    XQueryPointer(XtDisplay(toplevel), DefaultRootWindow(XtDisplay(toplevel)),
		  &root, &child, &rx, &ry, &wx, &wy, &mask);
  } while (!(mask & Button1Mask));

  childwin = child;

  output_value("Window ID %d is selected.", (int)childwin);
}

/*---------------------------------------------------------------------------*/
/* 親ウインドウの検索                                                        */
/*---------------------------------------------------------------------------*/

static void search_parent()
{
  Window r, p;
  Window * children;
  unsigned int n;

  if (childwin == DefaultRootWindow(XtDisplay(toplevel))) {
    output_message("This is root window.");
    return;
  }
  XQueryTree(XtDisplay(toplevel), childwin, &r, &p, &children, &n);
  XFree(children);
  childwin = p;

  if (p == DefaultRootWindow(XtDisplay(toplevel)))
    output_message("Root window.");
  else
    output_value("Window id is %d.", (int)childwin);
}

/*---------------------------------------------------------------------------*/
/* リストの初期化                                                            */
/*---------------------------------------------------------------------------*/

static void clear_sequence()
{
  ListClear(key_list);
  output_message("Clear OK.");
}

/*---------------------------------------------------------------------------*/
/* レコーディングの開始                                                      */
/*---------------------------------------------------------------------------*/

static void record_sequence()
{
  recording = 1;
  output_message("Start recording.");
}

/*---------------------------------------------------------------------------*/
/* レコーディング，リアルタイム再生の停止                                    */
/*---------------------------------------------------------------------------*/

static void stop_recording()
{
  recording = 0;
  timer_pointer = NULL;
  if (timeout_id) XtRemoveTimeOut(timeout_id);
  output_message("Stopped all.");
}

/*---------------------------------------------------------------------------*/
/* レコーディングしたデータの再生(リアルタイム再生)のタイムアウト用関数      */
/*---------------------------------------------------------------------------*/

static void play_proc(XtPointer p, XtIntervalId * id)
{
  Time timer;

  if (ListGetAction(timer_pointer)) {
    ev.type = KeyPress;
    ev.xkey.type = KeyPress;
  } else {
    ev.type = KeyRelease;
    ev.xkey.type = KeyRelease;
  }
  ev.xkey.send_event = True;
  ev.xkey.display = XtDisplay(toplevel);
  ev.xkey.window = childwin;
  ev.xkey.root = DefaultRootWindow(XtDisplay(toplevel));
  ev.xkey.state = ListGetState(timer_pointer);
  ev.xkey.keycode = XKeysymToKeycode(XtDisplay(toplevel),
				     ListGetKey(timer_pointer));
  if (ListGetAction(timer_pointer)) {
    XSendEvent(XtDisplay(toplevel), childwin, False, KeyPressMask, &ev);
    output_string("Key %s is pressed.",
		  XKeysymToString(ListGetKey(timer_pointer)));
  } else {
    XSendEvent(XtDisplay(toplevel), childwin, False, KeyReleaseMask, &ev);
    output_string("Key %s is released.",
		  XKeysymToString(ListGetKey(timer_pointer)));
  }

  timer = ListGetTimer(timer_pointer);
  timer_pointer = ListGetNext(timer_pointer);
  timeout_id = timer_pointer ?
    XtAddTimeOut(ListGetTimer(timer_pointer) - timer, play_proc, NULL) : 0;

  if (timer_pointer == NULL)
    output_message("Finished playing.");
}

/*---------------------------------------------------------------------------*/
/* レコーディングしたデータの再生(リアルタイム再生)                          */
/*---------------------------------------------------------------------------*/

static void play_sequence()
{
  if (timeout_id)
    output_message("Now, playing.");
  else {
    timer_pointer = ListGetStart(key_list);
    timeout_id = timer_pointer ? XtAddTimeOut(1000, play_proc, NULL) : 0;
    output_message("Start playing.");
  }
}

/*---------------------------------------------------------------------------*/
/* レコーディングしたデータを張り付け                                        */
/*---------------------------------------------------------------------------*/

static void repeat_sequence()
{
  ListPointer p;

  p = ListGetStart(key_list);

  while (p != NULL) {

    if (childwin) {
      ev.type = KeyPress;
      ev.xkey.type = KeyPress;
      ev.xkey.send_event = True;
      ev.xkey.display = XtDisplay(toplevel);
      ev.xkey.window = childwin;
      ev.xkey.root = DefaultRootWindow(XtDisplay(toplevel));
      ev.xkey.state = ListGetState(p);
      ev.xkey.keycode = XKeysymToKeycode(XtDisplay(toplevel), ListGetKey(p));
      if (ListGetAction(p))
	XSendEvent(XtDisplay(toplevel), childwin, False, KeyPressMask, &ev);
      else
	XSendEvent(XtDisplay(toplevel), childwin, False, KeyReleaseMask, &ev);
    }
    p = ListGetNext(p);
  }
  output_message("Repeated data.");
}

/*---------------------------------------------------------------------------*/
/* データのセーブ                                                            */
/*---------------------------------------------------------------------------*/

static void save_sequence()
{
  FILE * fp;
  ListPointer p;
  Time offset;

  p = ListGetStart(key_list);
  if (p == NULL) return;

  output_message("Saving...");
  fp = fopen(filename, "wt");
  offset = ListGetTimer(p);
  while (p != NULL) {
    fprintf(fp, "%d %d %d %d ",
	    (int)ListGetAction(p), (int)ListGetKey(p),
	    (int)ListGetState(p), (int)ListGetTimer(p) - offset);
    fprintf(fp, "%s\n", XKeysymToString(ListGetKey(p)));
    p = ListGetNext(p);
  }

  fclose(fp);

  output_message("Save OK!");
}

/*---------------------------------------------------------------------------*/
/* 終了                                                                      */
/*---------------------------------------------------------------------------*/

static void quit()
{
  if (child) {
    kill(child, SIGINT);
    child = 0;
  }
  exit (0);
}














/*****************************************************************************/
/* イベントハンドラ                                                          */
/*****************************************************************************/

/*---------------------------------------------------------------------------*/
/* イクスポーズ                                                              */
/*---------------------------------------------------------------------------*/

static void expose(Widget w, XtPointer client_data, XEvent * event,
		   Boolean * dispatch)
{
  output_message(NULL);
}

/*****************************************************************************/
/* シグナル処理                                                              */
/*****************************************************************************/

/*---------------------------------------------------------------------------*/
/* 子プロセスが終了したときのシグナル(SIGCHLD)のハンドラ                     */
/*---------------------------------------------------------------------------*/

static void child_exit(int value)
{
  output_message("Child process was killed.");
  child = 0;
  childwin = 0;
}


/*===========================================================================*/
/**/
/*===========================================================================*/

/*---------------------------------------------------------------------------*/
/* キーを押下                                                                */
/*---------------------------------------------------------------------------*/

static void key_press(Widget w, XtPointer client_data, XEvent * event,
		      Boolean * dispatch)
{
  KeySym key_sym;
  char * key_str;

  if (event->type == KeyPress) {
    ev.type = KeyPress;
    ev.xkey.type = KeyPress;
    ev.xkey.send_event = True;
    ev.xkey.display = XtDisplay(w);
    ev.xkey.window = childwin;
    ev.xkey.root = DefaultRootWindow(XtDisplay(w));
    ev.xkey.state = event->xkey.state;
    ev.xkey.keycode = event->xkey.keycode;
    key_sym = XKeycodeToKeysym(XtDisplay(w), event->xkey.keycode, 0);
    key_str = XKeysymToString(key_sym);
    if (childwin) {
      XSendEvent(XtDisplay(w), childwin, False, KeyPressMask, &ev);
      output_string("Key %s is pressed and sent.", key_str);
    } else
      output_string("Key %s is pressed.", key_str);

    if (recording)
      ListAddToEnd(key_list, 1, key_sym, event->xkey.state, event->xkey.time);
  }
}

/*---------------------------------------------------------------------------*/
/* キーを離した                                                              */
/*---------------------------------------------------------------------------*/

static void key_release(Widget w, XtPointer client_data, XEvent * event,
			Boolean * dispatch)
{
  KeySym key_sym;
  char * key_str;

  if (event->type == KeyRelease) {
    ev.type = KeyRelease;
    ev.xkey.type = KeyRelease;
    ev.xkey.send_event = True;
    ev.xkey.display = XtDisplay(w);
    ev.xkey.window = childwin;
    ev.xkey.root = DefaultRootWindow(XtDisplay(w));
    ev.xkey.state = event->xkey.state;
    ev.xkey.keycode = event->xkey.keycode;
    key_sym = XKeycodeToKeysym(XtDisplay(w), event->xkey.keycode, 0);
    key_str = XKeysymToString(key_sym);
    if (childwin) {
      XSendEvent(XtDisplay(w), childwin, False, KeyReleaseMask, &ev);
      output_string("Key %s is released and sent.", key_str);
    } else
      output_string("Key %s is released and sent.", key_str);
    if (recording)
      ListAddToEnd(key_list, 0, key_sym, event->xkey.state, event->xkey.time);
  }
}

int main(int argc, char * argv[])
{
  Widget select_button;
  Display * display;
  Window root;
  Window r, p;
  Window * children_before;
  Window * children_after;
  unsigned int n_before;
  unsigned int n_after;
  int dummy_argc = 0;
  FILE * fp;
  char buf[200];
  int i0, i1, i2, i3;

  toplevel = XtInitialize(argv[0], "XKeyWrap", NULL, 0, &dummy_argc, NULL);

  XtVaSetValues(toplevel, XtNinput, True, NULL);
  XtVaSetValues(toplevel, XtNwidth , WIDTH , NULL);
  XtVaSetValues(toplevel, XtNheight, HEIGHT, NULL);
  XtVaSetValues(toplevel, XtNtitle, "XKeyWrap", NULL);

  display = XtDisplay(toplevel);
  root = DefaultRootWindow(display);

  XQueryTree(display, root, &r, &p, &children_before, &n_before);

  if (argc > 1) {
    if ((child = fork()) == 0) {
      execvp(argv[2], argv + 2);
      exit (1);
    }
  }

  do {
    XQueryTree(display, root, &r, &p, &children_after, &n_after);
    childwin = diff_windows(children_before, n_before, children_after, n_after);
  } while (!childwin);

  font = XLoadFont(XtDisplay(toplevel), "8x16");
  font_gc = XCreateGC(XtDisplay(toplevel),
		      DefaultRootWindow(XtDisplay(toplevel)), 0, 0);
  XSetFont(XtDisplay(toplevel), font_gc, font);
  XSetFunction(XtDisplay(toplevel), font_gc, GXcopy);

  XtRealizeWidget(toplevel);

  XtAddEventHandler(toplevel, ExposureMask, False, expose, NULL);
  XtAddEventHandler(toplevel, KeyPressMask, False, key_press, NULL);
  XtAddEventHandler(toplevel, KeyReleaseMask, False, key_release, NULL);
  XtAddEventHandler(toplevel, ButtonPressMask, False, button_press, NULL);
  XtAddEventHandler(toplevel, PointerMotionMask, False, pointer_motion, NULL);

  signal(SIGCHLD, child_exit);

  output_value("Window ID %d is selected.", (int)childwin);

  key_list = ListCreate();

  if (argc > 0) filename = argv[1];
  else filename = DEFAULT_FILENAME;

  if ((fp = fopen(filename, "rt")) != NULL) {
    while (fgets(buf, 200, fp) != NULL) {
      sscanf(buf, "%d %d %d %d", &i0, &i1, &i2, &i3);
      ListAddToEnd(key_list, i0, i1, i2, i3);
    }
  }

  XtMainLoop();
}

/* End of Program */
