/*
 * Copyright (c) 1995 Sun Microsystems, Inc.
 * All rights reserved.
 * 
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the
 * above copyright notice and the following two paragraphs appear in
 * all copies of this software.
 * 
 * IN NO EVENT SHALL SUN MICROSYSTEMS, INC. BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
 * OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF SUN
 * MICROSYSTEMS, INC. HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * SUN MICROSYSTEMS, INC. SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THE SOFTWARE PROVIDED
 * HEREUNDER IS ON AN "AS IS" BASIS, AND SUN MICROSYSTEMS, INC. HAS NO
 * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
 * MODIFICATIONS.
 */

#include <dlfcn.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/audioio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <thread.h>

#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/cursorfont.h>

#define CLICK_NSECONDS  300000000 /* Double-click timeout (nanoseconds) */
#define PING_NSECONDS 10000000000 /* Ping timeout (nanoseconds) */
#define KC_PAUSE 28              /* X keycode for the Pause key */
#define KC_SCROLL_LOCK 30        /* X keycode for the Scroll Lock key */
#define KC_MUTE 52               /* X keycode for audio mute key */
#define KC_LOWER_VOLUME 9        /* X keycode for audio lower volume */
#define KC_RAISE_VOLUME 11       /* X keycode for audio raise key */
#define PLAYER_OFFSET 24         /* Offset to the frag info */
#define PLAYER_SIZE 70           /* Size of each player structure */

#define MIN(a,b) (a<b?a:b)
#define MAX(a,b) (a>b?a:b)

/* DOOM global variables */
int deathmatch, mousebfire, mousebstrafe, key_fire, usemouse;
char *player_names[4];

/* Frag information */
int players;
static int fragpipe, audioctl_fd;
static int *player0, *player1, *player2, *player3;
static int player0_save[4], player1_save[4], player2_save[4], player3_save[4];

/* Player names */
char *PlayerColors[] = 
{
  "DOOM_GREEN",
  "DOOM_INDIGO",
  "DOOM_BROWN",
  "DOOM_RED",
};
 
/* Local variables */
static void *x11_handle = NULL;
static int (*next_event)(Display *, XEvent *) = NULL;

static int keycode_grab, keycode_fire, keycode_pause,
  keycode_caps, keycode_numlock, keycode_lshift, keycode_rshift;
static Window doom_window;
static int grabbed = 0;
static int suppress_vertical = 0;
static int center_x, center_y;
static int curx, cury;


static void
init_keycodes(Display *display)
{
  keycode_grab = KC_SCROLL_LOCK;
  keycode_pause = KC_PAUSE;
  keycode_caps = XKeysymToKeycode(display, XK_Caps_Lock);
  keycode_numlock = XKeysymToKeycode(display, XK_Num_Lock);
  keycode_lshift = XKeysymToKeycode(display, XK_Shift_L);
  keycode_rshift = XKeysymToKeycode(display, XK_Shift_R);

  if (key_fire < 128)
    keycode_fire = XKeysymToKeycode(display, key_fire);
  else {
    switch (key_fire) {
    case 157: /* Control */
      keycode_fire = XKeysymToKeycode(display, XK_Control_L);
      break;
    case 172:
    case 173:
    case 174:
    case 175: /* Arrow keys */
      keycode_fire = XKeysymToKeycode(display, XK_Left + key_fire - 172);
      break;
    case 182: /* Shift */
      keycode_fire = XKeysymToKeycode(display, XK_Shift_L);
      break;
    case 184: /* Meta */
      keycode_fire = XKeysymToKeycode(display, XK_Meta_L);
      break;
    default:
      keycode_fire = XKeysymToKeycode(display, key_fire);
      break;
    }
  }
}

static void *
no_input(void *arg)
{
  pid_t parent = (pid_t)arg;
  struct sigaction oact;

  while (!next_event) {
    sigaction(SIGPOLL, NULL, &oact);
    if (oact.sa_handler)
      kill(parent, SIGPOLL);
    sleep(1);
  }
  return 0;
}

static void
init_players()
{
  int i;
  pid_t parent;
  char buf[BUFSIZ];
  char *name, *fragpipestr;
  sigset_t orig_mask, no_input_mask;

  player0 = &players + PLAYER_OFFSET;
  player1 = &players + PLAYER_OFFSET + PLAYER_SIZE;
  player2 = &players + PLAYER_OFFSET + (2 *PLAYER_SIZE);
  player3 = &players + PLAYER_OFFSET + (3 *PLAYER_SIZE);

  memset(player0_save, -1, 16);
  memset(player1_save, -1, 16);
  memset(player2_save, -1, 16);
  memset(player3_save, -1, 16);

  for (i=0; i<4; i++) {
    if (name = getenv(PlayerColors[i])) {
      sprintf(buf, "%s: ", name);
      player_names[i] = strdup(buf);
    }
  }

  fragpipestr = getenv("FRAGPIPE");
  if (fragpipestr)
    fragpipe = atoi(fragpipestr);

  /* Hack to fix the no input problem */
  sigfillset(&no_input_mask);
  thr_sigsetmask(SIG_SETMASK, &no_input_mask, &orig_mask);
  thr_create(NULL, 0, no_input, (void *)getpid(),
	     THR_DETACHED | THR_DAEMON, NULL);
  thr_sigsetmask(SIG_SETMASK, &orig_mask, NULL);
}

static void
check_x11_handle()
{
  if (x11_handle == NULL) {
    x11_handle = dlopen("libX11.so", RTLD_LAZY);
    if (x11_handle == NULL) {
      fprintf(stderr, "dlopen: %s\n", dlerror());
      exit(1);
    }
  }
}

Window
XCreateWindow(Display *display, Window parent, int x, int y,
	      unsigned int width, unsigned int height,
	      unsigned int bw, int depth, unsigned int class,
	      Visual *visual, unsigned long valuemask,
	      XSetWindowAttributes *attributes)
{
  static Window (*create_window)(Display *, Window, int, int,
			      unsigned int, unsigned int, unsigned int,
			      int, unsigned int, Visual *, unsigned long,
			      XSetWindowAttributes *) = NULL;
  Window w, root;

  if (create_window == NULL) {
    init_keycodes(display);
    init_players();

    check_x11_handle();
    create_window = (Window (*)(Display *, Window, int, int,
				unsigned int, unsigned int, unsigned int,
				int, unsigned int, Visual *, unsigned long,
				XSetWindowAttributes *))
      dlsym(x11_handle, "XCreateWindow");
    if (create_window == NULL) {
      fprintf(stderr, "dlsym XCreateWindow: %s\n", dlerror());
      exit(1);
    }
  }

  if (usemouse) {
    attributes->event_mask = attributes->event_mask |
      ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
  }

  w = (*create_window)(display, parent, x, y, width, height, bw, depth,
		       class, visual, valuemask, attributes);

  XGetGeometry(display, DefaultRootWindow(display),
	       &root, &x, &y, &width, &height, &bw, &depth);
  center_x = width/2;
  center_y = height/2;
  doom_window = w;

  return w;
}

int
XSelectInput(Display *display, Window w, long event_mask)
{
  static int (*select_input)(Display *, Window, long) = NULL;

  if (select_input == NULL) {
    check_x11_handle();
    select_input = (int (*)(Display *, Window, long))
      dlsym(x11_handle, "XSelectInput");
    if (select_input == NULL) {
      fprintf(stderr, "dlsym XSelectInput: %s\n", dlerror());
      exit(1);
    }
  }

  if (usemouse) {
    event_mask = event_mask |
      ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
  }

  return (*select_input)(display, w, event_mask);
}

static void
toggle_grab(Display *display)
{
  if (!grabbed) {
    if (XGrabPointer(display, doom_window, False,
		     ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
		     GrabModeAsync, GrabModeAsync, None, None, CurrentTime)
	!= GrabSuccess) {
      fprintf(stderr, "Pointer Grab failed\n");
    }
    else if (XGrabKeyboard(display, doom_window, False,
			   GrabModeAsync, GrabModeAsync, CurrentTime)
	     != GrabSuccess) {
      fprintf(stderr, "Keyboard Grab failed\n");
      XUngrabPointer(display, CurrentTime);
    }
    else {
      printf("Mouse is grabbed.  Press \"Scroll Lock\" to toggle grab.\n");
      grabbed = 1;
    }
  }
  else {
    XUngrabPointer(display, CurrentTime);
    XUngrabKeyboard(display, CurrentTime);
    printf("Mouse is ungrabbed.\n");
    grabbed = 0;
  }
}

static void
pause_game(Display *display)
{
  static Cursor watch_cursor = NULL;
  static Cursor null_cursor = NULL;

  XEvent event;
  XSetWindowAttributes attr;
  int was_grabbed;

  if (*(&players + PLAYER_SIZE))
    return;

  was_grabbed = grabbed;
  if (grabbed)
    toggle_grab(display);

  printf("Game has been paused\n");

  if (!watch_cursor) {
    Pixmap pix;
    XColor color;
    char data[1];

    color.pixel = 0;
    color.flags = 0;
    *data = 0;
    pix = XCreatePixmapFromBitmapData(display, doom_window,
				      data, 1, 1, 0, 0, 1);
    null_cursor = XCreatePixmapCursor(display, pix, pix,
				      &color, &color, 0, 0);
    watch_cursor = XCreateFontCursor(display, XC_watch);
  }

  attr.cursor = watch_cursor;
  XChangeWindowAttributes(display, doom_window, CWCursor, &attr);

  do {
    (*next_event)(display, &event);
  } while(event.type != KeyPress);
  /* Discard the KeyRelease event */
  (*next_event)(display, &event);

  attr.cursor = null_cursor;
  XChangeWindowAttributes(display, doom_window, CWCursor, &attr);

  printf("Game has been resumed\n");

  if (was_grabbed)
    toggle_grab(display);
}

static void
audio_config(int keycode)
{
  audio_info_t get_info;
  audio_info_t set_info;
  int status, gain;

  if (!audioctl_fd)
    audioctl_fd = open("/dev/audioctl", O_RDONLY);
  if (audioctl_fd == -1)
    return;

  AUDIO_INITINFO(&set_info);
  while ((status = ioctl(audioctl_fd, AUDIO_GETINFO, &get_info)) == -1 &&
	 errno == EINTR);
  if (status == -1)
    return;

  switch (keycode) {
  case KC_MUTE:
    set_info.output_muted = !get_info.output_muted;
    break;
  case KC_LOWER_VOLUME:
    if (get_info.play.gain == 255)
      gain = get_info.play.gain - 15;
    else
      gain = get_info.play.gain - 16;
    set_info.play.gain = MAX(0, gain);
    break;
  case KC_RAISE_VOLUME:
    gain = get_info.play.gain + 16;
    set_info.play.gain = MIN(AUDIO_MAX_GAIN, gain);
    break;
  }

  while (ioctl(audioctl_fd, AUDIO_SETINFO, &set_info) == -1 &&
	 errno == EINTR);
}

static void
print_frags()
{
  if (memcmp(player0, player0_save, 16) ||
      memcmp(player1, player1_save, 16) ||
      memcmp(player2, player2_save, 16) ||
      memcmp(player3, player3_save, 16)) {

    if (fragpipe) {
      char buf[256];

      sprintf(buf, "{%d %d %d %d} {%d %d %d %d} {%d %d %d %d} {%d %d %d %d}\n",
	      player0[0], player0[1], player0[2], player0[3],
	      player1[0], player1[1], player1[2], player1[3],
	      player2[0], player2[1], player2[2], player2[3],
	      player3[0], player3[1], player3[2], player3[3]);
      if (write(fragpipe, buf, strlen(buf)) == -1) {
	perror("fragpipe write");
	fragpipe = 0;
      }
    }

    if (!fragpipe) {
      printf("%12s %12s %12s %12s %12s %12s\n",
	     "",
	     player_names[0],
	     player_names[1],
	     player_names[2],
	     player_names[3],
	     "Total");
      
      printf("%12s %9d %12d %12d %12d   %12d\n",
	     player_names[0],
	     player0[0], player0[1], player0[2], player0[3],
	     player0[1] + player0[2] + player0[3] - player0[0]);
      
      printf("%12s %9d %12d %12d %12d   %12d\n",
	     player_names[1],
	     player1[0], player1[1], player1[2], player1[3],
	     player1[0] + player1[2] + player1[3] - player1[1]);
      
      printf("%12s %9d %12d %12d %12d   %12d\n",
	     player_names[2],
	     player2[0], player2[1], player2[2], player2[3],
	     player2[0] + player2[1] + player2[3] - player2[2]);
      
      printf("%12s %9d %12d %12d %12d   %12d\n",
	     player_names[3],
	     player3[0], player3[1], player3[2], player3[3],
	     player3[0] + player3[1] + player3[2] - player3[3]);
      
      printf("\n");
    }
      
    memcpy(player0_save, player0, 16);
    memcpy(player1_save, player1, 16);
    memcpy(player2_save, player2, 16);
    memcpy(player3_save, player3, 16);
  }
}

int
XNextEvent(Display *display, XEvent *event)
{
  static int dograb = 1;
  static int capsPress = 0;
  static hrtime_t clicktime = 0;
  static hrtime_t pingtime = 0;
  hrtime_t curtime;
  int retval;

  if (next_event == NULL) {
    check_x11_handle();

    next_event = (int (*)(Display *, XEvent *))dlsym(x11_handle, "XNextEvent");
    if (next_event == NULL) {
      fprintf(stderr, "dlsym XNextEvent: %s\n", dlerror());
      exit(1);
    }
  }
  retval = (*next_event)(display, event);

  if (!usemouse)
    dograb = 0;

  if (deathmatch)
    print_frags();

  if (fragpipe) {
    curtime = gethrtime();
    if (curtime - pingtime > PING_NSECONDS) {
      if (write(fragpipe, "ping\n", 5) == -1) {
	perror("fragpipe write");
	fragpipe = 0;
      }
      pingtime = curtime;
    }
  }

  switch(event->type) {
  case ButtonPress:
    if (dograb) {
      toggle_grab(display);
      dograb = 0;
    }
    if (usemouse && event->xbutton.button == mousebfire+1) {
      event->type = KeyPress;
      event->xkey.keycode = keycode_fire;
    }
    if (usemouse && event->xbutton.button == mousebstrafe+1) {
      curtime = gethrtime();
      if (curtime - clicktime < CLICK_NSECONDS) {
	suppress_vertical = !suppress_vertical;
	if (suppress_vertical)
	  printf("Vertical mouse motion disabled.\n");
	else
	  printf("Vertical mouse motion enabled.\n");
      }
      clicktime = curtime;
    }
    break;
  case ButtonRelease:
    if (dograb) {
      toggle_grab(display);
      dograb = 0;
    }
    if (usemouse && event->xbutton.button == mousebfire+1) {
      event->type = KeyRelease;
      event->xkey.keycode = keycode_fire;
    }
    break;
  case KeyPress:
    if (dograb) {
      toggle_grab(display);
      dograb = 0;
    }
    if (usemouse && event->xkey.keycode == keycode_grab)
      toggle_grab(display);
    else if (event->xkey.keycode == keycode_pause)
      pause_game(display);
    else if (event->xkey.keycode == keycode_caps ||
	     event->xkey.keycode == keycode_numlock) {
      event->xkey.keycode = keycode_lshift;
      capsPress = 1;
    }
    else if (event->xkey.keycode == keycode_lshift ||
	     event->xkey.keycode == keycode_rshift) {
      if (capsPress)
	event->type = KeyRelease;
    }
    else if (event->xkey.keycode == KC_MUTE ||
	     event->xkey.keycode == KC_LOWER_VOLUME ||
	     event->xkey.keycode == KC_RAISE_VOLUME) {
      audio_config(event->xkey.keycode);
    }
    break;
  case KeyRelease:
    if (event->xkey.keycode == keycode_caps ||
	event->xkey.keycode == keycode_numlock) {
      event->xkey.keycode = keycode_lshift;
      capsPress = 0;
    }
    else if (event->xkey.keycode == keycode_lshift ||
	     event->xkey.keycode == keycode_rshift) {
      if (capsPress)
	event->type = KeyPress;
    }
    break;
  case MotionNotify:
    if (dograb) {
      toggle_grab(display);
      dograb = 0;
    }
    if (grabbed) {
      if (event->xmotion.x_root != center_x ||
	  event->xmotion.y_root != center_y) {
	curx += (event->xmotion.x_root - center_x);
	if (!suppress_vertical)
	  cury += (event->xmotion.y_root - center_y);
	XWarpPointer(display, None, DefaultRootWindow(display),
		     0, 0, 0, 0, center_x, center_y);
      }

      event->xmotion.x = curx;
      event->xmotion.x_root = curx;
      event->xmotion.y = cury;
      event->xmotion.y_root = cury;
    }
    break;
  default:
    break;
  }

  return retval;
}
