/*
 * Copyright (C) 2002 Tugrul Galatali <tugrul@galatali.com>
 *
 * driver.c is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as 
 * published by the Free Software Foundation.
 *
 * driver.c 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
 */

#include <libgen.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <sys/param.h>

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

#include <GL/gl.h>
#include <GL/glx.h>

#include "driver.h"

#include "vroot.h"

xstuff_t *XStuff;

extern char *hack_name;
extern void hack_handle_opts (int argc, char **argv);
extern void hack_init (xstuff_t *);
extern void hack_reshape (xstuff_t *);
extern void hack_draw (xstuff_t *);
extern void hack_cleanup (xstuff_t *);

/*
 * display parameters 
 */
int rootWindow = False;
int frameTime = 0;
int be_nice = 0;
int signalled = 0;

void createWindow (int argc, char **argv)
{
	XVisualInfo *visualInfo;
	GLXContext context;

	XStuff->screen_num = DefaultScreen (XStuff->display);
	XStuff->rootWindow = RootWindow (XStuff->display, XStuff->screen_num);

	if (rootWindow || XStuff->existingWindow) {
		XWindowAttributes gwa;
		Visual *visual;
		XVisualInfo templ;
		int outCount;

		XStuff->window = XStuff->existingWindow ? XStuff->existingWindow : XStuff->rootWindow;

		XGetWindowAttributes (XStuff->display, XStuff->window, &gwa);
		visual = gwa.visual;
		XStuff->windowWidth = gwa.width;
		XStuff->windowHeight = gwa.height;

		templ.screen = XStuff->screen_num;
		templ.visualid = XVisualIDFromVisual (visual);

		visualInfo = XGetVisualInfo (XStuff->display, VisualScreenMask | VisualIDMask, &templ, &outCount);

		if (!visualInfo) {
			fprintf (stderr, "%s: can't get GL visual for window 0x%lx.\n", XStuff->commandLineName, (unsigned long)XStuff->window);
			exit (1);
		}
	} else {
		int attributeList[] = {
			GLX_RGBA,
			GLX_RED_SIZE, 1,
			GLX_GREEN_SIZE, 1,
			GLX_BLUE_SIZE, 1,
			GLX_DEPTH_SIZE, 1,
			GLX_DOUBLEBUFFER,
			0
		};
		XSetWindowAttributes swa;
		XSizeHints hints;
		XWMHints wmHints;

		visualInfo = NULL;

		if (!(visualInfo = glXChooseVisual (XStuff->display, XStuff->screen_num, attributeList))) {
			fprintf (stderr, "%s: can't open GL visual.\n", XStuff->commandLineName);
			exit (1);
		}

		swa.colormap = XCreateColormap (XStuff->display, XStuff->rootWindow, visualInfo->visual, AllocNone);
		swa.border_pixel = swa.background_pixel = swa.backing_pixel = BlackPixel (XStuff->display, XStuff->screen_num);
		swa.event_mask = KeyPressMask | StructureNotifyMask;

		XStuff->windowWidth = DisplayWidth (XStuff->display, XStuff->screen_num) / 3;
		XStuff->windowHeight = DisplayHeight (XStuff->display, XStuff->screen_num) / 3;

		XStuff->window =
			XCreateWindow (XStuff->display, XStuff->rootWindow, 0, 0, XStuff->windowWidth, XStuff->windowHeight, 0, visualInfo->depth, InputOutput, visualInfo->visual,
				       CWBorderPixel | CWBackPixel | CWBackingPixel | CWColormap | CWEventMask, &swa);

		hints.flags = USSize;
		hints.width = XStuff->windowWidth;
		hints.height = XStuff->windowHeight;

		wmHints.flags = InputHint;
		wmHints.input = True;

		XmbSetWMProperties (XStuff->display, XStuff->window, hack_name, hack_name, argv, argc, &hints, &wmHints, NULL);
	}

	context = glXCreateContext (XStuff->display, visualInfo, 0, GL_TRUE);
	if (!context) {
		fprintf (stderr, "%s: can't open GLX context.\n", XStuff->commandLineName);
		exit (1);
	}

	if (!glXMakeCurrent (XStuff->display, XStuff->window, context)) {
		fprintf (stderr, "%s: can't set GL context.\n", XStuff->commandLineName);
		exit (1);
	}

	XFree (visualInfo);
	XMapWindow (XStuff->display, XStuff->window);
}

void clearBuffers() {
	int i;
	XEvent event;

	for (i = 0; i < 4; i++) {
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
		glXSwapBuffers (XStuff->display, XStuff->window);

		while (XPending (XStuff->display)) {
			XNextEvent (XStuff->display, &event);
		}
	}
}

void mainLoop (void)
{
	int bFPS = False;
	XEvent event;
	Atom XA_WM_PROTOCOLS = XInternAtom (XStuff->display, "WM_PROTOCOLS", False);
	Atom XA_WM_DELETE_WINDOW = XInternAtom (XStuff->display, "WM_DELETE_WINDOW", False);
	struct timeval then, now, fps_time;
	int fps = 0;

	if (!rootWindow) {
		XSetWMProtocols (XStuff->display, XStuff->window, &XA_WM_DELETE_WINDOW, 1);
	}

	clearBuffers();

	gettimeofday (&now, NULL);
	while (!signalled) {
		hack_draw (XStuff);

		glXSwapBuffers (XStuff->display, XStuff->window);

		if (bFPS) {
			if (fps != -1)
				fps++;

			gettimeofday (&now, NULL);

			if (now.tv_sec > fps_time.tv_sec) {
				if (fps != -1) {
					printf ("%d fps\n", fps);
				}

				fps = 0;
				fps_time.tv_sec = now.tv_sec;
				fps_time.tv_usec = now.tv_usec;
			}
		}

		while (XPending (XStuff->display)) {
			KeySym keysym;
			char c = 0;

			XNextEvent (XStuff->display, &event);
			switch (event.type) {
			case ConfigureNotify:
				if ((int)XStuff->windowWidth != event.xconfigure.width || (int)XStuff->windowHeight != event.xconfigure.height) {
					XStuff->windowWidth = event.xconfigure.width;
					XStuff->windowHeight = event.xconfigure.height;

					clearBuffers();

					hack_reshape (XStuff);
				}

				break;
			case KeyPress:
				XLookupString (&event.xkey, &c, 1, &keysym, 0);

				if (c == 'f') {
					bFPS = !bFPS;

					if (bFPS) {
						fps = -1;
						gettimeofday (&fps_time, NULL);
					}
				}

				if (c == 'q' || c == 'Q' || c == 3 || c == 27)
					return;

				break;
			case ClientMessage:
				if (event.xclient.message_type == XA_WM_PROTOCOLS) {
					if (event.xclient.data.l[0] == XA_WM_DELETE_WINDOW) {
						return;
					}
				}
				break;
			case DestroyNotify:
				return;
			}
		}

		if (frameTime) {
			int frameTimeSoFar = 0;

			then = now;
			gettimeofday (&now, NULL);
			frameTimeSoFar = (now.tv_sec - then.tv_sec) * 1000000 + now.tv_usec - then.tv_usec;

			while (frameTimeSoFar < frameTime) {
				if (be_nice) {
/* nanosleep on Linux/i386 seems completely ineffective for idling for < 20ms */
/*
#ifdef HAVE_NANOSLEEP
					struct timespec hundreth;

					hundreth.tv_sec = 0;
					hundreth.tv_nsec = frameTime - frameTimeSoFar;

					nanosleep(&hundreth, NULL);
#endif
*/

/*
					usleep(frameTime - frameTimeSoFar);
*/

					struct timeval tv;

					tv.tv_sec = 0;
					tv.tv_usec = frameTime - frameTimeSoFar;
					select (0, 0, 0, 0, &tv);
				}

				gettimeofday (&now, NULL);
				frameTimeSoFar = (now.tv_sec - then.tv_sec) * 1000000 + now.tv_usec - then.tv_usec;
			}
		} else if (be_nice) {
			struct timeval tv;

			tv.tv_sec = 0;
			tv.tv_usec = 1000;
			select (0, 0, 0, 0, &tv);
		}
	}
}

void handle_global_opts (int c)
{
	switch (c) {
	case 'r':
		rootWindow = 1;

		break;
	case 'x':
		c = strtol_minmaxdef (optarg, 10, 1, 10000, 1, 100, "--maxfps: ");

		frameTime = 1000000 / c;

		break;
	case 'n':
		be_nice = 1;

		break;
	}
}

int strtol_minmaxdef (char *optarg, int base, int min, int max, int type, int def, char *errmsg)
{
	int result = strtol (optarg, (char **)NULL, base);

	if (result < min) {
		if (errmsg) {
			fprintf (stderr, errmsg);
			fprintf (stderr, "%d < %d, using %d instead.\n", result, min, type ? min : def);
		}

		return type ? min : def;
	}

	if (result > max) {
		if (errmsg) {
			fprintf (stderr, errmsg);
			fprintf (stderr, "%d > %d, using %d instead.\n", result, max, type ? max : def);
		}

		return type ? max : def;
	}

	return result;
}

void signalHandler (int sig)
{
	signalled = 1;
}

int main (int argc, char *argv[])
{
	struct sigaction sa;
	char *display_name = NULL;	/* Server to connect to */
	int i, j;

	XStuff = (xstuff_t *) malloc (sizeof (xstuff_t));
	XStuff->commandLineName = argv[0];

#ifdef BENCHMARK
	srandom (1);
#else
	srandom ((unsigned)time (NULL));
#endif

	XStuff->existingWindow = 0;
	for (i = 0; i < argc; i++) {
		if (!strcmp (argv[i], "-window-id")) {
			if ((argv[i + 1][0] == '0') && ((argv[i + 1][1] == 'x') || (argv[i + 1][1] == 'X'))) {
				XStuff->existingWindow = strtol ((char *)(argv[i + 1] + 2), (char **)NULL, 16);
			} else {
				XStuff->existingWindow = strtol ((char *)(argv[i + 1]), (char **)NULL, 10);
			}

			for (j = i + 2; j < argc; j++) {
				argv[j - 2] = argv[j];
			}

			argc -= 2;

			break;
		}
	}

	hack_handle_opts (argc, argv);

	XStuff->display = NULL;
	XStuff->window = 0;

	memset ((void *)&sa, 0, sizeof (struct sigaction));
	sa.sa_handler = signalHandler;
	sigaction (SIGINT, &sa, 0);
	sigaction (SIGPIPE, &sa, 0);
	sigaction (SIGQUIT, &sa, 0);
	sigaction (SIGTERM, &sa, 0);

	/*
	 * Connect to the X server. 
	 */
	if (NULL == (XStuff->display = XOpenDisplay (display_name))) {
		fprintf (stderr, "%s: can't connect to X server %s\n", XStuff->commandLineName, XDisplayName (display_name));
		exit (1);
	}

	createWindow (argc, argv);

	hack_init (XStuff);

	mainLoop ();

	/*
	 * Clean up. 
	 */
	if (XStuff->display) {
		if (XStuff->window) {
			hack_cleanup (XStuff);

			if (!((rootWindow) || (XStuff->existingWindow)))
				XDestroyWindow (XStuff->display, XStuff->window);
		}

		XCloseDisplay (XStuff->display);
	}

	return 0;
}
