/*
Copyright (c) 1998 Michael Haar, Ulrich Haar, Michael Thayer,
  Tobias Mueller, Tobias Lenz
All rights reserved.
Terroid Programming by Michael Haar, Michael Thayer, Ulrich Haar
Terroid Graphics by Ulrich Haar, Michael Haar
Terroid Music by Michael Thayer
Terroid Level design by Michael Haar, Ulrich Haar, Tobias Lenz
Terroid Beta testing by Tobias Mueller, Tobias Lenz

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice and credits, this unmodified list of conditions and the
   following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice and credits, this unmodified list of conditions and the
   following disclaimer in the documentation and/or other materials
   provided with the distribution.
3. Any credits displayed during programme execution must include the
   above or an equivalent credit list.
4. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.
5. The authors Michael Thayer and Michael Haar are to be informed,
   preferable by electronic mail, when this software is distributed
   commercially.  A single notification is required for any given
   distribution, and explicit copies of that distribution (e.g. for all
   resellers of a given Linux distribution).
6. If any modifications are made to the software, then a description of them
   must be included when the software is redistributed, or alternatively,
   the distributed binary or source code must be described as a product
   based on source code by Michael Haar, Michael Thayer and Ulrich Haar.

THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/*********************************\
* DN X Window sprite manipulation *
*   functions.                    *
*                                 *
* Copyright 1997 Michael Thayer   *
\*********************************/

/* Important functions:
	set_mode, exit_gfx, get,
	put, objput, Copy2Screen,
	cls, pal_set_colour,
	set_caption, SetColors

*/

#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <X11/Xlib.h>

#include <dn.h>

/* Local functions */

void get_screen_size(void) ;
BOOLEAN create_window(void) ;
void setup_gc(void) ;
void create_colourmap(void) ;
Pixmap create_mask(WORD x1, WORD y1, WORD x2, WORD y2) ;
void do_events(void) ;
void do_event_loop(void) ;
void do_key_stroke(void) ;
void do_keyboard(long until) ;
void handle_key_press(XKeyEvent key) ;
void handle_key_release(XKeyEvent key) ;
void handle_expose(void) ;
void update_time(Time stamp) ;
void set_one_colour(BYTE col, BYTE r, BYTE g, BYTE b) ;
void copy_frame_buffer(void) ;

/* Sprite image structure */

/* typedef struct {
	Pixmap image ;
	Pixmap mask ;
	int w ;
	int h ;
} GFXBLK ;
*/


/* Key buffer structure */

typedef struct {
	char key ;
	BOOLEAN pressed ;
	long timestamp ;
} kb_event ;


/* Globals */

	/* X specific globals */

	Display * jxdisp ;
	int jxscreen ;
	GC jxgc ;
	Window jxwin = 0 ;
	Pixmap jxbuffer = 0 ;
	Colormap jxcols = 0 ;
	char jxcaption[64] = "Digital Nightmares" ;
	XImage * jximage ;

	BOOLEAN use_image_buffer = FALSE ;
	RGB palette_contents[256] ;
	long int last_do_events = 0 ;
	struct timeval start_time = {0, 0} ;
	kb_event key_buffer[32] ;
	int buffer_start = 0 ;
	int buffer_end = 0 ;
	const buffer_len = 32 ;

	/* General DN globals */

	void * screen_addr = NULL ;
	int screen_width, screen_height ;
	WORD gfxmode, clip_x1, clip_y1, clip_x2, clip_y2 ;
	long screen_len ;
	BOOLEAN file_gefunden ;
	char key_pressed = 0 ;
	int keyFlags[256] ;

/* Simulates a retrace delay */

void wait_retrace(void)
{
	long i ;
	
	i = timer() ;
	while (i * 7 / 9 == timer() * 7 / 9) ;
}


/* Set up X display, game window (pseudocolour only) and graphics libs */

BOOLEAN set_mode(WORD mode)
{
	if (screen_addr) free(screen_addr) ;
	if (jxbuffer) XFreePixmap(jxdisp, jxbuffer) ;
	gfxmode = mode ;
	get_screen_size() ;
	if (jxwin != 0)
		XResizeWindow(jxdisp, jxwin, screen_width, screen_height) ;
	else {
		if (!create_window()) return(FALSE) ;
		create_colourmap() ;
	}
	screen_addr = calloc(1, screen_len * 4) ;
	jximage = XCreateImage(jxdisp, DefaultVisual(jxdisp, jxscreen), 8,
		ZPixmap, 0, screen_addr, screen_width, screen_height, 8,
		screen_width) ;
	jxbuffer = XCreatePixmap(jxdisp, jxwin, screen_width, screen_height,
		8) ;
	clip_x1 = clip_y1 = 0 ;
	clip_x2 = screen_width - 1 ;
	clip_y2 = screen_height - 1 ;
	return(TRUE) ;
}

/* Delete the game window, free X resources and disconnect from display */

void exit_gfx(void)
{
	XDestroyWindow(jxdisp, jxwin) ;
	if (jximage) {
		(* jximage).data = screen_addr ;
		XDestroyImage(jximage) ;
	}
	if (jxbuffer) XFreePixmap(jxdisp, jxbuffer) ;
	if (jxcols) XFreeColormap(jxdisp, jxcols) ;
	XCloseDisplay(jxdisp) ;
	jxwin = jxbuffer = jxcols = 0 ;
	screen_addr = NULL ; 
}


/* Creates a pixmap and a mask from specified data in buffer at screen_addr */

void get(WORD x1, WORD y1, WORD x2, WORD y2, GFXBLK * blk)
{
	if (x1 > x2) swap(& x1, & x2) ;
	if (y1 > y2) swap(& y1, & y2) ;
	++x2 ;
	++y2 ;
	(* blk).image = 0 ;
	(* blk).mask = create_mask(x1, y1, x2, y2) ;
	if (! (* blk).mask) return ;
	(* blk).image = XCreatePixmap(jxdisp, jxwin, x2 - x1, y2 - y1, 8) ;
	if (! (* blk).image) {
		XFreePixmap(jxdisp, (* blk).mask) ;
		return ;
	}
	(* jximage).data = screen_addr ;
	XPutImage(jxdisp, (* blk).image, jxgc, jximage, x1, y1, 0, 0, x2 - x1,
		y2 - y1) ;
	(* blk).w = x2 - x1 ;
	(* blk).h = y2 - y1 ;
}


/* Frees the ressources used by a GFXBLK */

void free_blk(GFXBLK * blk)
{
	XFreePixmap(jxdisp, (* blk).image) ;
	XFreePixmap(jxdisp, (* blk).mask) ;
	(* blk).w = 0 ;
	(* blk).h = 0 ;
}


/* Copies the sprite in blk to the off-screen buffer jxbuffer, clips sides */

void put(WORD x, WORD y, GFXBLK * blk)
{
	XGCValues v ;
	int xplus, yplus, w, h ;

	v.function = GXcopy ;
	v.clip_mask = None ;
	XChangeGC(jxdisp, jxgc, GCClipMask | GCFunction, & v) ;
	xplus = max(0, clip_x1 - x) ;
	yplus = max(0, clip_y1 - y) ;
	w = min((* blk).w, clip_x2 - x) - xplus ;
	h = min((* blk).h, clip_y2 - y) - yplus ;
	XCopyArea(jxdisp, (* blk).image, jxbuffer, jxgc, xplus, yplus, w, h,
		x + xplus, y + yplus) ;
}


/* Copies the sprite in blk to the off-screen buffer jxbuffer, full clipping */

void objput(WORD x, WORD y, GFXBLK * blk)
{
	XGCValues v ;
	int xplus, yplus, w, h ;

	v.function = GXcopy ;
	v.clip_mask = (* blk).mask ;
	v.clip_x_origin = x ;
	v.clip_y_origin = y ;
	XChangeGC(jxdisp, jxgc, GCClipMask | GCFunction | GCClipXOrigin
		| GCClipYOrigin, & v) ;
	xplus = max(0, clip_x1 - x) ;
	yplus = max(0, clip_y1 - y) ;
	w = min((* blk).w, clip_x2 - x - xplus) ;
	h = min((* blk).h, clip_y2 - y - yplus) ;
	XCopyArea(jxdisp, (* blk).image, jxbuffer, jxgc, xplus, yplus, w, h,
		x + xplus, y + yplus) ;
	v.clip_mask = None ;
	XChangeGC(jxdisp, jxgc, GCClipMask, & v) ;	
}


/* (Should) add the sprite blk pixel-wise to the off-screen buffer jxbuffer */
/*
void addput(WORD x, WORD y, GFXBLK * blk)
{
	XGCValues v ;

	v.function = GXadd ;
	XChangeGC(jxdisp, jxgc, GCClipMask | GCFunction, & v) ;
	XCopyArea(jxdisp, (blk *).image, jxbuffer, jxgc, 0, 0, (blk *).w,
		(blk *).h, x, y) ;
}
*/


/* Draws a box onto the off-screen buffer */

void box(WORD x1, WORD y1, WORD x2, WORD y2, BYTE c, BOOLEAN fill,
	BOOLEAN rainbow)
{
	XGCValues v ;

	v.foreground = c ;
	XChangeGC(jxdisp, jxgc, GCForeground, & v) ;
	if (x1 > x2) swap(& x1, & x2) ;	/* Start clipping sh*te */
	if (y1 > y2) swap(& y1, & y2) ;
	x1 = max(x1, clip_x1) ;
	x2 = min(x2, clip_x2) ;
	y1 = max(y1, clip_y1) ;
	y2 = min(y2, clip_y2) ;
	x1 = min(x1, x2) ;
	y1 = min(y1, y2) ;		/* End clipping sh*te */
	if (fill)
		XFillRectangle(jxdisp, jxbuffer, jxgc, x1, y1, x2 - x1,
			y2 - y1) ;
	else
		XDrawRectangle(jxdisp, jxbuffer, jxgc, x1, y1, x2 - x1,
			y2 - y1) ;
}


/* Display screen buffer, and force a complete screen update */

void Copy2Screen(void)
{
	XCopyArea(jxdisp, jxbuffer, jxwin, jxgc, 0, 0, screen_width,
		screen_height, 0, 0) ;
	use_image_buffer = FALSE ;
	XSync(jxdisp, FALSE) ;
	do_events() ;
}


/* Clear screen buffer */

void cls(BYTE colour)
{
	XGCValues v ;

	v.foreground = colour ;
	XChangeGC(jxdisp, jxgc, GCForeground, & v) ;
	XFillRectangle(jxdisp, jxbuffer, jxgc, 0, 0, screen_width,
		screen_height) ;
}


/* Set a colour in the game palette */

void pal_set_colour(BYTE col, BYTE red, BYTE green, BYTE blue)
{
	set_one_colour(col, red, green, blue) ;
	XFlush(jxdisp) ;
}


/* Sets the title in the game window */

void delay(int msecs)
{
	int i ;

	i = timer() * 11 ;
	while ((timer() * 11 + msecs) < i) ;
}

char * itoa(int value, char * buffer, int radix)
{
	int rem, sign, length ;
	const char numchars[16] =
	  {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
	  'A', 'B', 'C', 'D', 'E', 'F'} ;

	length = 0 ;
	rem = value ;
	while (rem > 0)
	  {
		rem /= radix ;
		++length ;
	  }
	sign = 0 ;
	if (value < 0)
	  {
		sign = 1 ;
		value = 0 - value ;
		++length ;
	  }
	if (value == 0)
	  {
		buffer[0] = '0' ;
		buffer[1] = 0 ;
	  }
	else
	  {
		buffer[length] = 0 ;
		--length ;
	  }

	while (value > 0)
	  {
		rem = value % radix ;
		value /= radix ;
		if ((-1 < rem) && (rem < 16))
		  {
			* (buffer + length) = numchars[rem] ;
		  }
		else
		  {
			* (buffer + length) = '*' ;
		  }
		--length ;
	  }
	if (sign)
	  {
		buffer[0] = '-' ;
	  }
	return(buffer) ;
} ;


/* reads the last keypress from the keyboard buffer */

char get_key(void)
{
	while (buffer_end != buffer_start) {
		do_key_stroke() ;
		if (key_buffer[(buffer_start - 1) % buffer_len].pressed)
			return(key_buffer[(buffer_start - 1) % buffer_len].key) ;
	}
	return(0) ;
}


/* Handles all keyboard events up 'till the given time */

void do_keyboard(long until)
{
	while (buffer_end != buffer_start) {
		if (key_buffer[(buffer_end - 1) % 32].timestamp > until)
			return ;
	do_key_stroke() ;
	}
}


/* Processes a keystroke, and removes it from the queue */

void do_key_stroke(void)
{
	if (buffer_end == buffer_start) return ;
	if (key_buffer[buffer_start].pressed)
		keyFlags[(int) key_buffer[buffer_start].key] = 1 ;
	else
		keyFlags[(int) key_buffer[buffer_start].key] = 0 ;
	buffer_start++ ;
	buffer_start %= buffer_len ;
}


/* Guesses (!) the current X server time */

long timer(void)
{
	struct timeval t ;

	gettimeofday(& t, NULL) ;
	if (start_time.tv_sec == 0) start_time = t ;
	return((t.tv_sec - start_time.tv_sec) * 91 +
		(t.tv_usec - start_time.tv_usec) / 11000) ;
}


/* Rekursives Suchen einer Datei: */

void FindFile(const char *path, char *fname)
{
	DIR * onlyone_dir ;
	struct dirent * find ;
	struct stat filestat ;
	char searchpath[8192] ;

	onlyone_dir = opendir(path) ;
	if (!onlyone_dir)
	  {
		return ;
	  }
	find = readdir(onlyone_dir) ;
	while((find != NULL) && (!file_gefunden))
	  {
		/* Irgendwas gefunden: */
		strcpy(searchpath, path);
		strcat(searchpath, "/") ;
		strcat(searchpath, (* find).d_name);
		if (!stat(searchpath, & filestat))
		  if (filestat.st_mode & S_IRUSR)
		  {
			if(strcmp((* find).d_name, "..") && strcmp((* find).d_name, ".")
				&& (filestat.st_mode & S_IFDIR))
			  {
				FindFile(searchpath, fname);
			  } /* if */
			else if(!strcasecmp( (* find).d_name, fname) )
			  {
				/* Datei irgendwo gefunden: */
				strcpy(searchpath, path);
				strcat(searchpath, "/") ;
				strcat(searchpath, (* find).d_name);
				strcpy(fname, searchpath);
				file_gefunden = TRUE;
			  } /* else */
		  } /* if */
		/* und weiter: */
		find = readdir(onlyone_dir) ;
	  } /* while */
	closedir(onlyone_dir) ;
}

BOOLEAN keyTest(int k)
{
	do_events() ;
	return(keyFlags[(k & 255)] != 0) ;
}

void set_caption(char * s)
{
	strncpy(jxcaption, s, 64) ;
	if (jxwin)
		XStoreName(jxdisp, jxwin, jxcaption) ;
}


/* Set a block of entries in the game palette */

void SetColors(RGB * pal, int start, int anz)
{
	int i;

	for(i = start ; i < start + anz ; i++)
		set_one_colour(i,pal[i].r,pal[i].g,pal[i].b) ;
	XFlush(jxdisp) ;
}


void GetPalette(RGB * pal)
{
	int i ;
	
	for (i = 255 ; i >=0 ; i--) {
		pal[i].r = palette_contents[i].r ;
		pal[i].g = palette_contents[i].g ;
		pal[i].b = palette_contents[i].b ;
	}
}


/* Initialises the screen size for set_mode() */

void get_screen_size(void)
{
	switch (gfxmode) {
		case 0x13:
			screen_width = 320 ;
			screen_height = 200 ;
			screen_len = 320 * 200 / 4 ;
			break ;
		case 0x100:
			screen_width = 640 ;
			screen_height = 400 ;
			screen_len = 640 * 400 / 4 ;
			break ;
		case 0x101:
			screen_width = 640 ;
			screen_height = 480 ;
			screen_len = 640 * 480 / 4 ;
			break ;
		default:
			screen_width = 80 ;
			screen_height = 25 ;
			screen_len = 80 * 25 / 4 ;
	}
}


/* Connect to server and create and display window */

BOOLEAN create_window(void)
{
	if ((jxdisp = XOpenDisplay(NULL)) == NULL) {
		error_text = "Cannot access X Window display" ;
		return(FALSE) ;
	}
	jxscreen = DefaultScreen(jxdisp) ;
	if (DefaultDepth(jxdisp, jxscreen) != 8) { /* PseudoColour? */
		error_text = "This game requires a PseudoColour (256 colour) screen" ;
		return(FALSE) ;
	}
	setup_gc() ;
	jxwin = XCreateSimpleWindow(jxdisp, DefaultRootWindow(jxdisp), 0, 0,
		screen_width, screen_height, 0, 0, 0) ;
	XStoreName(jxdisp, jxwin, jxcaption) ;
	XMapWindow(jxdisp, jxwin) ;
	XFlush(jxdisp) ;
	XSelectInput(jxdisp, jxwin, KeyPressMask | KeyReleaseMask
		| ExposureMask) ;
	return(TRUE) ;
}


/* Sets up the GC for use */

void setup_gc(void)
{
	XGCValues v ;

	jxgc = DefaultGC(jxdisp, jxwin) ;
	v.graphics_exposures = FALSE ;
	XChangeGC(jxdisp, jxgc, GCGraphicsExposures, & v) ;
}


/* Create and install colourmap */

void create_colourmap(void)
{
	jxcols = XCreateColormap(jxdisp, jxwin, DefaultVisual(jxdisp,
		jxscreen), AllocAll) ;
	XSetWindowColormap(jxdisp, jxwin, jxcols) ;
	XSetWMColormapWindows(jxdisp, jxwin, NULL, 0) ;
}


/* Swaps two words */

inline void swapw(WORD * a, WORD * b)
{
	WORD i ;
	
	i = * a ;
	* a = * b ;
	* b = i ;
}


/* Create drawing mask of specified portion of screen_addr buffer */
/* IMPORTANT: XCreateBitmapFromData pads by BYTE, not by bit */

Pixmap create_mask(WORD x1, WORD y1, WORD x2, WORD y2)
{
	int i, j, ofs, w ;
	char * data ;
	Pixmap mask ;

	if (x2 < x1) swapw(& x1, & x2) ;
	if (y2 < y1) swapw(& y1, & y2) ;
	w = (x2 - x1 + 7) & ~7 ;
	data = calloc((y2 - y1) * w / 8, 1) ;
	if (!data) return (0) ;
	for (i = y1 ; i < y2 ; i++)
		for (j = x1 ; j < x2 ; j++) {
			ofs = (i - y1) * w + (j - x1) ;
			if (((BYTE *) screen_addr)[i * screen_width + j])
				data[ofs / 8] |= 1 << (ofs & 7) ;
		}
	mask = XCreateBitmapFromData(jxdisp, jxwin, data, x2 - x1,
		y2 - y1) ;
	free(data) ;
	return(mask) ;
}


/* Clear the X event queue (not _too_ often!) and dispatch events */

void do_events(void)
{
	long int i ;

	if (last_do_events == timer()) return ;
	if (mod_play) play_sound() ; /* Candidate for replacement! */
	i = XPending(jxdisp) ;
	while (i > 0) {
		do_event_loop() ;
		--i ;
	}
	last_do_events = timer() ;
}


/* X application event loop */

void do_event_loop(void)
{
	XEvent e ;

	XNextEvent(jxdisp, & e) ;
	switch (e.type) {
		case KeyPress:
			handle_key_press(e.xkey) ;
			break ;
		case KeyRelease:
			handle_key_release(e.xkey) ;
			break ;
		case GraphicsExpose:
			handle_expose() ;
		default:
	}
}


/* Dispatch a KeyPress event message, update time */

void handle_key_press(XKeyEvent key)
{
	int i ;

	update_time(key.time) ;
	i = XKeycodeToKeysym(jxdisp, key.keycode, 0) & 255 ;
	if ((i >= 'a') && (i <= 'z'))
		i += 'A' - 'a' ;
	keyFlags[i] = 1 ;
	key_buffer[buffer_end].key = i ;
	key_buffer[buffer_end].pressed = TRUE ;
	key_buffer[buffer_end].timestamp = key.time / 11 ;
	buffer_end++ ;
	buffer_end %= 32 ;
	if (buffer_start == buffer_end) do_key_stroke() ;
}


/* Dispatch a KeyRelease message */

void handle_key_release(XKeyEvent key)
{
	int i ;

	update_time(key.time) ;
	i = XKeycodeToKeysym(jxdisp, key.keycode, 0) & 255 ;
	if ((i >= 'a') && (i <= 'z')) i += 'A' - 'a' ;
	keyFlags[i] = 0 ;
	key_buffer[buffer_end].key = i ;
	key_buffer[buffer_end].pressed = FALSE ;
	key_buffer[buffer_end].timestamp = key.time / 11 ;
	buffer_end++ ;
	buffer_end %= 32 ;
	if (buffer_start == buffer_end) do_key_stroke() ;
}


/* Repaint the screen after an expose event */

void handle_expose(void)
{
	if (use_image_buffer)
		copy_frame_buffer() ;
	else
		Copy2Screen() ;
}


/* Refines the guess at the current server time */

void update_time(Time stamp)
{
	unsigned long t ;

	t = timer() ;
	if (t < stamp / 11) {
		stamp -= t * 11 ;
		start_time.tv_usec -= stamp % 1000 * 1000 ;
		if (start_time.tv_usec < 0) {
			--start_time.tv_sec ;
			start_time.tv_usec += 1000000 ;
		}
		start_time.tv_sec -= stamp / 1000 ;
	}
}


/* Set one colour in the windows colourmap (does not flush display) */

void set_one_colour(BYTE col, BYTE r, BYTE g, BYTE b)
{
	XColor c ;
	
	palette_contents[col].r = r ;
	palette_contents[col].g = g ;
	palette_contents[col].b = b ;
	c.pixel = col ;
	c.red = r << 10 ;
	c.green = g << 10 ;
	c.blue = b << 10 ;
	c.flags = DoRed | DoGreen | DoBlue ;
	XStoreColor(jxdisp, jxcols, & c) ;
}


void copy_frame_buffer(void)
{
	(* jximage).data = screen_addr ;
	XPutImage(jxdisp, jxwin, jxgc, jximage, 0, 0, 0, 0, screen_width,
		screen_height) ;
	use_image_buffer = TRUE ;
	do_events() ;
}
