/***********************************************************
*  joystick4doom - Playing Linux DOOM with a joystick      *
*----------------------------------------------------------*
*  1994  Artsoft Development                               *
*        Holger Schemel                                    *
*        33659 Bielefeld-Senne                             *
*        Telefon: (0521) 493245                            *
*        eMail: aeglos@valinor.ms.sub.org                  *
*               aeglos@uni-paderborn.de                    *
*               q99492@pbhrzx.uni-paderborn.de             *
*----------------------------------------------------------*
*  programming date: 02.10.94                              *
*----------------------------------------------------------*
*  Fine tuned and enhanced October 19, 1994, by            *
*     Sam Lantinga (slouken@cs.ucdavis.edu)                *
***********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

#define XK_MISCELLANY
#define XK_LATIN1

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <X11/Intrinsic.h>
#include <X11/keysymdef.h>

#include <linux/joystick.h>

#define PIDFILE		"/tmp/joystick4doom.pid"
#ifndef JOYSTICK_DAT
#define JOYSTICK_DAT	"/usr/local/lib/joystick.dat"
#endif

#define PAUSETIME	100000

#define RELEASED	0
#define PRESSED		1
#define HOLDDOWN	2
#define UNPRESSED	3

#define JB_PRESSED(b)	(button[b]==PRESSED || button[b]==HOLDDOWN)
#define JB_UNPRESSED(b)	(button[b]==RELEASED || button[b]==UNPRESSED)
#define ABS(x)		((x)>0 ? (x) : (-x))

/* Thresholds (in percentages) */
#define KEYRELEASE_AT	10
#define KEYPRESS_AT	10
#define RUNNING_AT	90

#define JK_SHIFT	0
#define JK_ALT		1
#define JK_META		2
#define JK_CONTROL	3
#define JK_SPACE	4
#define JK_ESCAPE	5

Display			*display;
char			*progname;
int			joystick_device;
char			*device_name;
struct JS_DATA_TYPE	js;
int			button[2];
int			xpos_left, xpos_middle, xpos_right, xpos_actual;
int			ypos_upper,ypos_middle, ypos_lower, ypos_actual;
int			i,j;

/* Keep track of where the joystick is */
#define	CENTERED	0x00
#define CUTRIGHT	0x01
#define CUTLEFT		0x02
#define CUTUP		0x10
#define CUTDOWN		0x20
#define RUNNING		0x40

#define SET(var, flag)		(var |= flag)
#define UNSET(var, flag)	(var &= ~flag)
#define ISSET(var, flag)	((var|flag) == var)
#define ISRIGHT(var)		ISSET(var, CUTRIGHT)
#define ISLEFT(var)		ISSET(var, CUTLEFT)
#define ISUP(var)		ISSET(var, CUTUP)
#define ISDOWN(var)		ISSET(var, CUTDOWN)
#define ISRUNNING(var)		ISSET(var, RUNNING)

char *str2lower(char *);
void GetJoystick(void);
int GetMovement(int, int, int);
void WaitButton(void);
void SendKeyEvent(int, int, KeySym);
void Calibrate(void);

int main(int argc, char *argv[])
{
  int up,down,left,right;
  int kp=KEYPRESS_AT;		/* threshold for pressed cursor key */
  int kr=KEYRELEASE_AT;		/* threshold for released cursor key */
  int kh=RUNNING_AT;		/* threshold for high speed motion */
  int key[2];
  char joystate=CENTERED;	/* Current state of the joystick */
  void quit();
  FILE *pidfile; int pid;
  FILE *datfile;

  struct keys
  {
    char *keyname;
    int pressed, released;
    KeySym key;
  };
  static struct keys joykey[6] =
  {
    "shift", 0,ShiftMask, XK_Shift_L,
    "alt", 0,Mod1Mask, XK_Meta_L,
    "meta", 0,Mod2Mask, XK_Alt_L,
    "control", 0,ControlMask, XK_Control_L,
    "space", 0,0, XK_space,
    "escape", 0,0, XK_Escape
  };

  fprintf(stdout, "Joystick4Doom, 1994 by Artsoft Development\n");

  /* Kill an existing joystick process if requested */
  if ( (argc == 2) && (strcmp(argv[1], "-k") == 0) ) {
    if ( (pidfile=fopen(PIDFILE, "r")) != NULL ) {
      fscanf(pidfile, "%d", &pid);
      (void) kill(pid, SIGINT);
    }
    exit(0);
  }
  /* Calibrate the joystick if requested */
  if ( (argc == 3) && (strcmp(argv[1], "-c") == 0) ) {
    device_name=argv[2];
    if ( (joystick_device=open(device_name,O_RDONLY)) < 0 ) {
      perror(device_name);
      exit(-1);
    }
    Calibrate();
    printf("Joystick calibrated.\n");
    exit(0);
  }

  if (argc!=4 || *argv[1]=='?')
  {
    fprintf(stderr, "Usage: %s {-c device}|{-k}\nor..\n" ,argv[0]);
    fprintf(stderr, "Usage: %s device key1 key2\n",argv[0]);
    fprintf(stderr, "   device: the device of your joystick, e.g. /dev/js0\n");
    fprintf(stderr, "   key1/2: the key you want to map to button 1/2;\n");
    fprintf(stderr, "           this can be Shift, Alt/Meta, Control, Space or Escape\n");
    exit(-1);
  }

  for(i=0;i<2;i++)
  {
    key[i]=-1;
    for(j=0;j<6;j++)
    {
      if (!strcmp(str2lower(argv[i+2]),joykey[j].keyname))
	key[i]=j;
    }
    if (key[i]<0)
    {
      fprintf(stderr, "%s: unknown key\n",argv[i+2]);
      exit(-1);
    }
  }

  progname=argv[0];
  device_name=argv[1];

  if ((joystick_device=open(device_name,O_RDONLY))<0)
  {
    perror(device_name);
    exit(-1);
  }

  /* connect to X server */
  if (!(display=XOpenDisplay(NULL)))
  {
    fprintf(stderr, "%s: cannot connect to X server\n", progname);
    exit(-1);
  }

  button[0]=button[1]=RELEASED;

  if ( (datfile=fopen(JOYSTICK_DAT, "r")) == NULL )
    Calibrate();
  else {
    fscanf(datfile, "%d %d %d\n", &xpos_left, &xpos_middle, &xpos_right);
    fscanf(datfile, "%d %d %d\n", &ypos_upper, &ypos_middle, &ypos_lower);
    fclose(datfile);
  }
#ifdef PRECALIBRATED
  xpos_left   = 15;
  ypos_upper  = 25;
  xpos_right  = 1920;
  ypos_lower  = 1950;
  xpos_middle = 750;
  ypos_middle = 600;
#endif

  /* Register this process */
  if ( (pidfile=fopen(PIDFILE, "w")) != NULL ) {
    fprintf(pidfile, "%d\n", getpid());
    fclose(pidfile);
  }
  signal(SIGINT, quit);
  signal(SIGTERM,  quit);
  fprintf(stdout, "(Interrupt with 'Control-C' after playing...)\n");

  for(;;)
  {
#ifdef DEBUG
	printf("loop: <- %%%d ^ %%%d V %%%d  %%%d ->\n", left, up, down, right);
#endif
    GetJoystick();

    left =  GetMovement(xpos_middle, xpos_left,  xpos_actual);
    right = GetMovement(xpos_middle, xpos_right, xpos_actual);
    up =    GetMovement(ypos_middle, ypos_upper, ypos_actual);
    down =  GetMovement(ypos_middle, ypos_lower, ypos_actual);

    for(i=0;i<2;i++)
    {
      if (button[i]==PRESSED) {
	SendKeyEvent(KeyPress,joykey[key[i]].pressed,joykey[key[i]].key);
#ifdef DEBUG
	printf("Sent V key #%d\n", i);
#endif
      }
      if (button[i]==RELEASED) {
	SendKeyEvent(KeyRelease,joykey[key[i]].released,joykey[key[i]].key);
#ifdef DEBUG
	printf("Sent ^ key #%d\n", i);
#endif
      }
    }

    /* Check to see if joystick is pushed far enough for us to run */
    if ( !ISRUNNING(joystate) ) {
      if ( (up>kh) || (left>kh) || (right>kh) || (down>kh) ) {
        SendKeyEvent(KeyPress,0,XK_Shift_L);
        SET(joystate, RUNNING);
#ifdef DEBUG
        printf("Sent V <Shift>\n");
#endif
      }
    } else {
      if ( (up<kh) && (left<kh) && (right<kh) && (down<kh) ) {
        SendKeyEvent(KeyRelease,ShiftMask,XK_Shift_L);
        UNSET(joystate, RUNNING);
#ifdef DEBUG
        printf("Sent ^ <Shift>\n");
#endif
      }
    }

    /* Crosscheck motion of joystick with current state of the keys */
    if (!ISLEFT(joystate) && (left>kp)) {
      SendKeyEvent(KeyPress,0,XK_Left);
      SET(joystate, CUTLEFT);
#ifdef DEBUG
	printf("Sent V Left\n");
#endif
    }
    if (!ISRIGHT(joystate) && (right>kp)) {
      SendKeyEvent(KeyPress,0,XK_Right);
      SET(joystate, CUTRIGHT);
#ifdef DEBUG
	printf("Sent V Right\n");
#endif
    }
    if (!ISUP(joystate) && (up>kp)) {
      SendKeyEvent(KeyPress,0,XK_Up);
      SET(joystate, CUTUP);
#ifdef DEBUG
	printf("Sent V Up\n");
#endif
    }
    if (!ISDOWN(joystate) && (down>kp)) {
      SendKeyEvent(KeyPress,0,XK_Down);
      SET(joystate, CUTDOWN);
#ifdef DEBUG
	printf("Sent V Down\n");
#endif
    }

    if (ISLEFT(joystate) && (left<kr)) {
      SendKeyEvent(KeyRelease,0,XK_Left);
      UNSET(joystate, CUTLEFT);
#ifdef DEBUG
	printf("Sent ^ Left\n");
#endif
    }
    if (ISRIGHT(joystate) && (right<kr)) {
      SendKeyEvent(KeyRelease,0,XK_Right);
      UNSET(joystate, CUTRIGHT);
#ifdef DEBUG
	printf("Sent ^ Right\n");
#endif
    }
    if (ISUP(joystate) && (up<kr)) {
      SendKeyEvent(KeyRelease,0,XK_Up);
      UNSET(joystate, CUTUP);
#ifdef DEBUG
	printf("Sent ^ Up\n");
#endif
    }
    if (ISDOWN(joystate) && (down<kr)) {
      SendKeyEvent(KeyRelease,0,XK_Down);
      UNSET(joystate, CUTDOWN);
#ifdef DEBUG
	printf("Sent ^ Down\n");
#endif
    }

    usleep(PAUSETIME);
  }
  quit();
}

void quit() {
  (void) unlink(PIDFILE);
  XCloseDisplay(display);
  exit(0);
}

char *str2lower(char *input)
{
  int i,len;
  char output[100];

  len=strlen(input);
  if (len>99)
    input[len=99]=0;

  for(i=0;i<len;i++)
    output[i]=tolower(input[i]);
  output[i]=0;
  return(output);
}

void Calibrate(void)
{
  FILE *datfile;

  fprintf(stdout, "Move joystick to the upper left and press any button!\n");
  WaitButton();
  xpos_left =js.x;
  ypos_upper=js.y;
  fprintf(stdout, "xpos_left = %d\nypos_upper = %d\n", js.x,  js.y);

  fprintf(stdout, "Move joystick to the lower right and press any button!\n");
  WaitButton();
  xpos_right=js.x;
  ypos_lower=js.y;
  fprintf(stdout, "xpos_right = %d\nypos_lower = %d\n", js.x,  js.y);

  fprintf(stdout, "Move joystick to the middle and press any button!\n");
  WaitButton();
  xpos_middle=js.x;
  ypos_middle=js.y;
  fprintf(stdout, "xpos_middle = %d\nypos_middle = %d\n", js.x,  js.y);

  /* Record calibration in joystick.dat */
  if ( (datfile=fopen(JOYSTICK_DAT, "w")) == NULL ) {
    fprintf(stdout, "Warning: Can't open %s for writing.\n", JOYSTICK_DAT);
    return;
  }
  fprintf(datfile, "%d %d %d\n", xpos_left, xpos_middle, xpos_right);
  fprintf(datfile, "%d %d %d\n", ypos_upper, ypos_middle, ypos_lower);
  fclose(datfile);
}

void GetJoystick()
{
  if (read(joystick_device,&js,JS_RETURN) != JS_RETURN)
  {
    perror(device_name);
    exit(-1);
  }

  for(i=0;i<2;i++)
  {
    if (js.buttons & (1<<i))
      button[i] = (JB_UNPRESSED(i) ? PRESSED : HOLDDOWN);
    else
      button[i] = (JB_PRESSED(i) ? RELEASED : UNPRESSED);
  }

  xpos_actual=js.x;
  ypos_actual=js.y;
}

int GetMovement(int middle, int margin, int actual)
{
  long range, pos;
  int percentage;

  range=ABS(margin-middle);
  pos=actual-middle;
  percentage=(int)(pos*100/range);

  return(percentage);
}

void WaitButton()
{
  while(JB_UNPRESSED(0) && JB_UNPRESSED(1))
    GetJoystick();
  while(JB_PRESSED(0) || JB_PRESSED(1))
    GetJoystick();
}

void SendKeyEvent(int type, int state, KeySym key)
{
  long mask = (type==KeyPress ? KeyPressMask : KeyReleaseMask);
  int keycode = XKeysymToKeycode(display,key);
  static XKeyEvent keyevent =
  {
    0,
    0L,True,
    (Display *)NULL,
    (Window)0L,(Window)0L,(Window)0L,
    (Time)0L,
    0,0, 0,0,
    0, 0,
    True
  };

  keyevent.type=type;
  keyevent.state=state;
  keyevent.keycode=keycode;

  XSendEvent(display,InputFocus,False,mask,(XEvent *)&keyevent);
  XFlush(display);
}
