/*	Public domain	*/

/*
 * Agar's interactive test suite.
 */

#include "agartest.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <agar/config/ag_unicode.h>
#include <agar/config/have_freetype.h>
#include <agar/config/have_fontconfig.h>
#include <agar/config/have_gettext.h>
#include <agar/config/have_glu.h>
#include <agar/config/have_iconv.h>
#include <agar/config/have_jpeg.h>
#include <agar/config/have_opengl.h>
#include <agar/config/have_png.h>
#include <agar/config/have_portaudio.h>
#include <agar/config/have_sdl2.h>
#include <agar/config/have_sdl.h>
#include <agar/config/have_sndfile.h>
#include <agar/config/have_x11.h>
#include <agar/config/have_xinerama.h>

#include <agar/core/agsi.h>

#include "config/have_agar_au.h"
#include "config/have_agar_math.h"
#include "config/datadir.h"

extern const AG_TestCase buttonsTest;
extern const AG_TestCase checkboxTest;
extern const AG_TestCase configsettingsTest;
extern const AG_TestCase consoleTest;
extern const AG_TestCase customwidgetTest;
extern const AG_TestCase fixedresTest;
extern const AG_TestCase focusingTest;
extern const AG_TestCase fontsTest;
extern const AG_TestCase fspathsTest;
extern const AG_TestCase imageloadingTest;
extern const AG_TestCase keyeventsTest;
extern const AG_TestCase loaderTest;
extern const AG_TestCase maximizedTest;
extern const AG_TestCase minimalTest;
extern const AG_TestCase paletteTest;
extern const AG_TestCase paneTest;
extern const AG_TestCase radioTest;
extern const AG_TestCase rendertosurfaceTest;
extern const AG_TestCase scrollbarTest;
extern const AG_TestCase scrollviewTest;
extern const AG_TestCase socketsTest;
extern const AG_TestCase surfaceTest;
extern const AG_TestCase tableTest;
extern const AG_TestCase textboxTest;
extern const AG_TestCase textdlgTest;
extern const AG_TestCase threadsTest;
extern const AG_TestCase unitconvTest;
extern const AG_TestCase widgetsTest;
extern const AG_TestCase windowsTest;
#if defined(AG_TIMERS) && defined(AG_HAVE_FLOAT)
extern const AG_TestCase compositingTest;
#endif
#ifdef AG_UNICODE
extern const AG_TestCase charsetsTest;
#endif
#if defined(HAVE_AGAR_AU) && !defined(_WIN32)
extern const AG_TestCase audioTest;
#endif
#ifdef HAVE_OPENGL
extern const AG_TestCase glviewTest;
#endif
#ifdef AG_TIMERS
extern const AG_TestCase objsystemTest;
extern const AG_TestCase timeoutsTest;
#endif
#ifdef AG_USER
extern const AG_TestCase userTest;
#endif
#ifdef HAVE_AGAR_MATH
extern const AG_TestCase bezierTest;
extern const AG_TestCase mathTest;
extern const AG_TestCase plottingTest;
extern const AG_TestCase stringTest;
#endif

/* Autorun "widgets" when no test specified on the command-line. */
#define AUTORUN_WIDGETS

const AG_TestCase *testCases[] = {
	&buttonsTest,
	&checkboxTest,
	&configsettingsTest,
	&consoleTest,
	&customwidgetTest,
	&fixedresTest,
	&focusingTest,
	&fontsTest,
	&fspathsTest,
	&imageloadingTest,
	&keyeventsTest,
	&loaderTest,
	&maximizedTest,
	&minimalTest,
	&paletteTest,
	&paneTest,
	&radioTest,
	&rendertosurfaceTest,
	&scrollbarTest,
	&scrollviewTest,
	&socketsTest,
	&surfaceTest,
	&tableTest,
	&textboxTest,
	&textdlgTest,
	&threadsTest,
	&unitconvTest,
	&widgetsTest,
	&windowsTest,
#if defined(HAVE_AGAR_AU) && !defined(_WIN32)
	&audioTest,
#endif
#if defined(AG_TIMERS) && defined(AG_HAVE_FLOAT)
	&compositingTest,
#endif
#ifdef AG_UNICODE
	&charsetsTest,
#endif
#ifdef HAVE_OPENGL
	&glviewTest,
#endif
#ifdef AG_TIMERS
	&objsystemTest,
	&timeoutsTest,
#endif
#ifdef AG_USER
	&userTest,
#endif
#ifdef HAVE_AGAR_MATH
	&bezierTest,
	&mathTest,
	&plottingTest,
	&stringTest,
#endif
	NULL
};

/* Build options */
const char *agarBuildOpts[] = {
#ifdef AG_DEBUG
	"DEBUG  ",
#endif
#ifdef AG_ENABLE_DSO
	"DSO  ",
#endif
#ifdef AG_LEGACY
	"LEGACY  ",
#endif
#ifdef AG_NAMESPACES
	"NAMESPACES  ",
#endif
#ifdef AG_SERIALIZATION
	"SERIALIZATION  ",
#endif
#ifdef AG_THREADS
	"THREADS  ",
#endif
#ifdef AG_TIMERS
	"TIMERS  ",
#endif
#ifdef AG_TYPE_SAFETY
	"TYPE_SAFETY  ",
#endif
#ifdef AG_UNICODE
	"UNICODE  ",
#endif
#ifdef AG_USER
	"USER  ",
#endif
#ifdef AG_VERBOSITY
	"VERBOSITY  ",
#endif
	NULL
};

/* Build library support */
const char *agarBuildLibs[] = {
#ifdef HAVE_FREETYPE
	"FREETYPE  ",
#endif
#ifdef HAVE_FONTCONFIG
	"FONTCONFIG  ",
#endif
#ifdef HAVE_GETTEXT
	"GETTEXT  ",
#endif
#ifdef HAVE_GLU
	"GLU  ",
#endif
#ifdef HAVE_ICONV
	"ICONV  ",
#endif
#ifdef HAVE_JPEG
	"JPEG  ",
#endif
#ifdef HAVE_OPENGL
	"OPENGL  ",
#endif
#ifdef HAVE_PNG
	"PNG  ",
#endif
#ifdef HAVE_PORTAUDIO
	"PORTAUDIO  ",
#endif
#ifdef HAVE_SDL2
	"SDL2  ",
#endif
#ifdef HAVE_SDL
	"SDL  ",
#endif
#ifdef HAVE_SNDFILE
	"SNDFILE  ",
#endif
#ifdef HAVE_X11
	"X11  ",
#endif
#ifdef HAVE_XINERAMA
	"XINERAMA  ",
#endif
	NULL
};

TAILQ_HEAD_(ag_test_instance) tests;		/* Running tests */
AG_Window *winMain;				/* Main window */
AG_Statusbar *statusBar;
AG_Label *status;
AG_Console *console = NULL;
AG_Button *btnTest;
char consoleBuf[1024];

static void RunBench(AG_Event *);

static void
SelectedTest(AG_Event *event)
{
	AG_TlistItem *it = AG_TLISTITEM_PTR(1);
	AG_TestCase *tc = it->p1;

	AG_ButtonText(btnTest, _("Run " AGSI_BOLD "%s"), tc->name);
	AG_LabelText(status, _("Run %s (%s)."), tc->name, tc->descr);
	if (tc->test != NULL || tc->testGUI != NULL) {
		AG_WidgetEnable(btnTest);
	} else {
		AG_WidgetDisable(btnTest);
	}
}

#ifdef AG_THREADS
static void *
RunBenchmarks(void *arg)
{
	AG_TestInstance *ti = arg;
	AG_Console *console = ti->console;
	int rv;

	if (!AG_OBJECT_VALID(console) || !AG_CONSOLE_ISA(console)) {
		return (NULL);
	}
	rv = ti->tc->bench(ti);

	if (rv == 0) {
		AG_ConsoleMsg(console, _("%s: Success"), ti->tc->name);
	} else {
		AG_ConsoleMsg(console, _("%s: Failed (%s)"), ti->tc->name, AG_GetError());
	}
	free(ti);
	return (NULL);
}
#endif

static AG_TestInstance *
CreateTestInstance(AG_TestCase *tc)
{
	AG_TestInstance *ti;

	if ((ti = TryMalloc(tc->size)) == NULL) {
		goto fail;
	}
	ti->name = tc->name;
	ti->tc = tc;
	ti->flags = 0;
	ti->score = 1.0;
	ti->console = console;
	ti->win = NULL;

	if (tc->init != NULL &&
	    tc->init(ti) == -1) {
		AG_ConsoleMsg(console, _("%s: Failed: %s"), tc->name,
		    AG_GetError());
		goto fail;
	}
	return (ti);
fail:
	AG_LabelTextS(status, AG_GetError());
	return (NULL);
}

static void
RequestTestClose(AG_Event *event)
{
	AG_Window *win = AG_WINDOW_PTR(1);
	
	AG_PostEvent(win, "window-close", NULL);
}

static void
RunTest(AG_Event *event)
{
	AG_Tlist *tl = AG_TLIST_PTR(1);
	AG_Driver *drv = AGWIDGET(winMain)->drv;
	AG_DriverClass *drvClass = AGDRIVER_CLASS(drv);
	AG_TestCase *tc = AG_TlistSelectedItemPtr(tl);
	AG_TestInstance *ti;

	if (tc == NULL)
		return;

	if (tc->flags & AG_TEST_OPENGL) {
		if (!(drvClass->flags & AG_DRIVER_OPENGL)) {
			AG_TextMsg(AG_MSG_ERROR,
			    _("The `%s' test requires OpenGL.\n"
			      "Current driver (%s) has no GL support"),
			      tc->name, drvClass->name);
			return;
		}
	}
	if (tc->flags & AG_TEST_SDL) {
		if (!(drvClass->flags & AG_DRIVER_SDL)) {
			AG_TextMsg(AG_MSG_ERROR,
			    _("The `%s' test requires SDL.\n"
			      "Current driver (%s) has no SDL support"),
			      tc->name, drvClass->name);
			return;
		}
	}

	if ((ti = CreateTestInstance(tc)) == NULL)
		return;

	if (tc->test != NULL) {
		AG_ConsoleMsg(console, _("Running test: %s..."), tc->name);
		ti->score = 100.0;
		if (tc->test(ti) == 0) {
			if (ti->score != 100.0) {
				AG_ConsoleMsg(console, _("%s: Success (%f%%)"),
				    tc->name, ti->score);
			} else {
				AG_ConsoleMsg(console, _("%s: Success"), tc->name);
			}
		} else {
			AG_ConsoleMsg(console, _("%s: Failed (%s)"), tc->name,
			    AG_GetError());
			AG_LabelTextS(status, AG_GetError());
			goto fail;
		}
	}
	if (tc->bench != NULL) {
		AG_RedrawOnTick(console, 250);   /* XXX workaround */
		RunBench(event);
	}
	if (tc->testGUI != NULL) {
		AG_Window *win;

		if ((win = AG_WindowNew(0)) == NULL) {
			AG_LabelTextS(status, AG_GetError());
			return;
		}
		AG_WindowSetCaption(win, "agartest: %s", ti->name);
		AG_SetEvent(win, "window-close", TestWindowClose, "%p", ti);
	
		if (tc->testGUI(ti, win) == 0) {
			ti->win = win;
			AG_ConsoleMsg(console, _("%s: Interactive test started"),
			    tc->name);

			AG_SeparatorNewHoriz(win);

			ti->closeBtn = AG_ButtonNewFn(win, AG_BUTTON_HFILL, _("Close this test"),
			    RequestTestClose, "%p", win);

			AG_WindowAttach(winMain, win);
			AG_WindowShow(win);
		} else {
			AG_ConsoleMsg(console, _("%s: Failed to start (%s)"),
			    tc->name, AG_GetError());
			AG_ObjectDetach(win);
			goto fail;
		}
		TAILQ_INSERT_TAIL(&tests, ti, instances);
	} else {
		free(ti);
	}
	return;
fail:
	free(ti);
}

static void
RunBench(AG_Event *event)
{
	AG_Tlist *tl = AG_TLIST_PTR(1);
	AG_TestCase *tc = AG_TlistSelectedItemPtr(tl);
	AG_TestInstance *ti;

	if (tc == NULL || tc->bench == NULL)
		return;
	
	if ((ti = CreateTestInstance(tc)) == NULL)
		return;

	{
#ifdef AG_THREADS
		AG_Thread th;

		AG_ThreadCreate(&th, RunBenchmarks, ti);
#else
		if (tc->bench(ti) == 0) {
			if (!AG_OBJECT_VALID(console)) {
				return;
			}
			AG_ConsoleMsg(console, _("%s: Success"), tc->name);
		} else {
			if (!AG_OBJECT_VALID(console)) {
				return;
			}
			AG_ConsoleMsg(console, _("%s: Failed (%s)"), tc->name,
			    AG_GetError());
			AG_LabelTextS(status, AG_GetError());
		}
		free(ti);
#endif
	}
}

static void
TestWindowDetached(AG_Event *event)
{
	AG_TestInstance *ti = AG_PTR(1);
	
	TAILQ_REMOVE(&tests, ti, instances);
	if (ti->tc->destroy != NULL) {
		ti->tc->destroy(ti);
	}
	free(ti);
}

/* Close an interactive test. */
void
TestWindowClose(AG_Event *event)
{
	AG_TestInstance *ti = AG_PTR(1);
	
	AG_ConsoleMsg(console, _("Test %s: terminated"), ti->name);
	AG_SetEvent(ti->win, "window-detached", TestWindowDetached, "%p", ti);
	AG_ObjectDetach(ti->win);
}

/* Write a message to the test console (format string). */
AG_ConsoleLine *
TestMsg(void *obj, const char *fmt, ...)
{
	AG_TestInstance *ti = obj;
	AG_ConsoleLine *ln;
	va_list args;
	char *s;

	va_start(args, fmt);
	AG_Vasprintf(&s, fmt, args);
	ln = AG_ConsoleMsgS(ti->console, s);
	va_end(args);
	free(s);
	return (ln);
}

/* Write a message to the test console (C string). */
AG_ConsoleLine *
TestMsgS(void *obj, const char *s)
{
	AG_TestInstance *ti = obj;

	return AG_ConsoleMsgS(ti->console, s);
}

#if (defined(i386) || defined(__i386__) || defined(__x86_64__))
# define HAVE_RDTSC
static __inline__ Uint64
rdtsc(void)
{
	Uint64 rv;

	__asm __volatile(".byte 0x0f, 0x31" : "=A" (rv));
	return (rv);
}
#else
# undef HAVE_RDTSC
#endif

/* Execute a benchmark module (called from bench() op) */
void
TestExecBenchmark(void *obj, AG_Benchmark *bm)
{
	AG_TestInstance *ti = obj;
	AG_Console *cons = ti->console;
	Uint i, j, fIdx;
#if defined(HAVE_64BIT)
	Uint64 t1, t2;
	Uint64 tTot, tRun;
#else
	Uint32 t1, t2;
	Uint32 tTot, tRun;
#endif

	for (fIdx = 0; fIdx < bm->nFuncs; fIdx++) {
		char pbuf[64];
		AG_BenchmarkFn *bfn = &bm->funcs[fIdx];
		AG_ConsoleLine *cl;

		bfn->clksMax = -1;
		if (!AG_OBJECT_VALID(cons)) {
			return;
		}
		cl = AG_ConsoleMsg(cons, "\t%s: ...", bfn->name);
		if (cl == NULL) {
			continue;
		}
#ifdef HAVE_RDTSC
		if (agCPU.ext & AG_EXT_TSC) {
			for (i = 0, tTot = 0; i < bm->runs; i++) {
retry:
				t1 = rdtsc();
				for (j = 0; j < bm->iterations; j++) {
					bfn->run(ti, bfn->arg);
				}
				t2 = rdtsc();
				tRun = (t2 - t1) / bm->iterations;
			
				Snprintf(pbuf, sizeof(pbuf),
				    "\t%s: %lu clks [%i/%i]",
				    bfn->name,
				    (Ulong)tRun, i, bm->runs);
				AG_ConsoleMsgEdit(cl, pbuf);

				if (bm->maximum > 0 && tRun > bm->maximum) {
					Snprintf(pbuf, sizeof(pbuf),
					    "\t%s: <preempted>", bfn->name);
					AG_ConsoleMsgEdit(cl, pbuf);
					goto retry;
				}
				bfn->clksMax = AG_MAX(bfn->clksMax,tRun);
				bfn->clksMin = (bfn->clksMin > 0) ?
				               AG_MIN(bfn->clksMin,tRun) :
					       tRun;
				tTot += tRun;
			}
			bfn->clksAvg = (tTot / bm->runs);
			Snprintf(pbuf, sizeof(pbuf), "\t%s: %lu clks [%i]",
			    bfn->name, (Ulong)bfn->clksAvg, bm->runs);
			AG_ConsoleMsgEdit(cl, pbuf);
		} else
#endif /* HAVE_RDTSC */
		{
			for (i = 0, tTot = 0; i < bm->runs; i++) {
				t1 = AG_GetTicks();
				for (j = 0; j < bm->iterations; j++) {
					bfn->run(ti, bfn->arg);
				}
				t2 = AG_GetTicks();
				tRun = (t2 - t1);
				Snprintf(pbuf, sizeof(pbuf),
				    "\t%s: %lu ticks [%i/%i]",
				    bfn->name, (Ulong)tRun, i, bm->runs);
				AG_ConsoleMsgEdit(cl, pbuf);
				bfn->clksMax = AG_MAX(bfn->clksMax,tRun);
				bfn->clksMin = (bfn->clksMin > 0) ?
				               AG_MIN(bfn->clksMin,tRun) :
					       tRun;
				tTot += tRun;
			}
			Snprintf(pbuf, sizeof(pbuf), "\t%s: %lu ticks [%i]",
			    bfn->name, (Ulong)bfn->clksAvg, bm->runs);
			AG_ConsoleMsgEdit(cl, pbuf);
		}
	}
}

#ifdef AG_UNICODE
static void
RunUnicodeBrowser(AG_Event *event)
{
	AG_Window *win;

	if ((win = AG_DEV_UnicodeBrowser()) == NULL) {
		return;
	}
	AG_WindowSetPosition(win, AG_WINDOW_MIDDLE_RIGHT, 1);
	AG_SetFontSize(win, "150%");
	AG_WindowShow(win);
}
#endif

#ifdef AG_TIMERS
static void RunDriversBrowser(AG_Event *event) { AG_DEV_Browser(&agDrivers); }
static void RunClassInfo(AG_Event *event) { AG_DEV_ClassInfo(); }
static void RunFontsInfo(AG_Event *event) { AG_DEV_FontInfo(); }

static void
RunTimerInspector(AG_Event *event)
{
	AG_Window *win;

	if ((win = AG_DEV_TimerInspector()) == NULL) {
		return;
	}
	AG_WindowAttach(winMain, win);
	AG_WindowSetPosition(win, AG_WINDOW_MIDDLE_LEFT, 1);
	AG_SetFontSize(win, "80%");
	AG_WindowShow(win);
}

# ifdef AG_DEBUG
static void
DoDebugger(void)
{
	AG_Window *win;

	if ((win = AG_GuiDebugger(agWindowFocused)) != NULL)
		AG_WindowShow(win);
}
static void
RunDebugger(AG_Event *event)
{
	DoDebugger();
}
# endif
#endif /* AG_TIMERS */

static void
DoStyleEditor(void)
{
	AG_Window *win;

	if ((win = AG_StyleEditor(agWindowFocused)) != NULL)
		AG_WindowShow(win);
}

static void
RunStyleEditor(AG_Event *event)
{
	DoStyleEditor();
}

/* Redirect AG_Debug() and AG_Verbose() to the AG_Console. */
static int
ConsoleWrite(const char *msg)
{
	if (console == NULL) {
		return (0);
	}
	AG_Strlcat(consoleBuf, msg, sizeof(consoleBuf));
	if (strchr(msg, '\n') != NULL) {
		char *line, *pBuf = consoleBuf;

		while ((line = AG_Strsep(&pBuf, "\n")) != NULL) {
			if (line[0] == '\0') {
				continue;
			}
			AG_ConsoleMsgS(console, line);
		}
		consoleBuf[0] = '\0';
	}
	return (1);
}

static void
ConsoleWindowDetached(AG_Event *event)
{
	AG_SetVerboseCallback(NULL);
	AG_SetDebugCallback(NULL);
	console = NULL;
}

/* Show information about a test module */
static void
TestInfo(AG_Event *event)
{
	const AG_TestCase *tc = AG_cPTR(1);

	AG_TextMsg(AG_MSG_INFO,
	    _("Test: " AGSI_BOLD "%s" AGSI_RST "\n"
	      "Agar >= %s required.\n"
	      AGSI_ITALIC "%s" AGSI_RST),
	    tc->name,
	    tc->minVer ? tc->minVer : "1.0",
	    _(tc->descr));
}

/* Display the source code for a test module. */
static void
TestViewSource(AG_Event *event)
{
	char path[AG_PATHNAME_MAX];
	char file[AG_FILENAME_MAX];
	const AG_TestCase *tc = AG_cPTR(1);
	AG_Textbox *tb;
	AG_Window *win;
	char *s;
	FILE *f;
	AG_Size size;

	Strlcpy(file, tc->name, sizeof(file));
	Strlcat(file, ".c", sizeof(file));

	if (AG_ConfigFind(AG_CONFIG_PATH_DATA, file, path, sizeof(path)) != 0) {
		AG_TextMsgFromError();
		return;
	}
	if ((f = fopen(path, "r")) == NULL) {
		AG_TextError(_("Could not open %s"), file);
		return;
	}

	if ((win = AG_WindowNew(0)) == NULL) {
		return;
	}
	AG_SetFontFamily(win, "monoalgue");
	tb = AG_TextboxNew(win, AG_TEXTBOX_MULTILINE | AG_TEXTBOX_EXPAND |
	                        AG_TEXTBOX_READONLY, NULL);
	fseek(f, 0, SEEK_END);
	size = (AG_Size)ftell(f);
	fseek(f, 0, SEEK_SET);
	s = Malloc(size);
	fread(s, size, 1, f);
	fclose(f);
	s[size] = '\0';

	AG_TextboxBindASCII(tb, s, size);
	AG_WindowSetGeometry(win, -1, -1, 800,480);
	AG_WindowShow(win);
}

static void
TestPopupMenu(AG_Event *event)
{
	AG_Tlist *tl = AG_SELF();
	const AG_TestCase *tc = AG_TlistSelectedItemPtr(tl);
	AG_PopupMenu *pm;

	if (tc == NULL) {
		return;
	}
	pm = AG_PopupNew(tl);

	AG_MenuSeparator(pm->root);

	AG_MenuAction(pm->root, _("Test Information"), agIconMagnifier.s,
	    TestInfo, "%Cp", tc);

	AG_MenuAction(pm->root, _("View Source"), agIconDoc.s,
	    TestViewSource, "%Cp", tc);
	
	AG_MenuSeparator(pm->root);

	if (tc->test || tc->testGUI) {
		AG_MenuAction(pm->root,
		    tc->testGUI ? _("Run test") : _("Run test (on console)"),
		    agIconGear.s,
		    RunTest, "%p", tl);
	}
	if (tc->bench) {
		AG_MenuAction(pm->root, _("Run benchmark"), agIconGear.s,
		    RunBench, "%p", tl);
	}

	AG_PopupShow(pm);
}

static void
ZoomIn(AG_Event *event)
{
	AG_ZoomIn();
}

static void
ZoomOut(AG_Event *event)
{
	AG_ZoomOut();
}

static void
ZoomReset(AG_Event *event)
{
	AG_ZoomReset();
}

static void
EditPreferences(AG_Event *event)
{
	AG_DEV_ConfigShow();
}

int
main(int argc, char *argv[])
{
	char *driverSpec = NULL, *fontSpec = NULL, *styleSheet = NULL, *optArg;
	AG_Window *win;
	AG_Tlist *tl;
	const AG_TestCase **pTest;
	AG_Menu *menu;
	AG_MenuItem *mi;
	AG_Pane *pane;
	AG_Box *hBox;
	int c, i, optInd;
	Uint initFlags = AG_VERBOSE;
	int noConsoleRedir=0;

	TAILQ_INIT(&tests);

	while ((c = AG_Getopt(argc, argv, "CDWqd:s:t:v?hp:", &optArg, &optInd)) != -1) {
		switch (c) {
		case 'C':
			noConsoleRedir = 1;
			break;
		case 'D':
			agDebugLvl = 2;
			break;
		case 'W':
			initFlags |= AG_SOFT_TIMERS;
			break;
		case 'q':
			agDebugLvl = 0;
			initFlags &= ~(AG_VERBOSE);
			break;
		case 'd':
			driverSpec = optArg;
			break;
		case 's':
			styleSheet = optArg;
			break;
		case 't':
			fontSpec = optArg;
			break;
		case 'v':
			{
				AG_AgarVersion av;

				AG_GetVersion(&av);
				if (av.release) {
					printf("agar %d.%d.%d (\"%s\", %s)\n",
					    av.major, av.minor, av.patch,
					    av.release, AG_MEMORY_MODEL_NAME);
				} else {
					printf("agar %d.%d.%d (beta, %s)\n",
					    av.major, av.minor, av.patch,
					    AG_MEMORY_MODEL_NAME);
				}
				return (0);
			}
			break;
		case 'p':
			break;
		case '?':
		case 'h':
		default:
			printf("Usage: agartest [-bCWqv] [-d driver] [-s stylesheet] [-t font] [test1 test2 ...]\n");
			return (1);
		}
	}
	if (AG_InitCore("agartest", initFlags) == -1) {
		goto fail;
	}
#ifdef _WIN32
	AG_ConfigAddPathS(AG_CONFIG_PATH_FONTS, "..\\gui\\fonts");
	AG_ConfigAddPathS(AG_CONFIG_PATH_FONTS, "..\\..\\fonts");
#endif
	if (fontSpec != NULL) {
		AG_TextParseFontSpec(fontSpec);
	}
	if (AG_InitGraphics(driverSpec) == -1)
		goto fail;

	if (styleSheet != NULL &&
	    AG_LoadStyleSheet(NULL, styleSheet) == NULL)
		goto fail;

	/* Redirect AG_Verbose() and AG_Debug() output to the AG_Console. */
	consoleBuf[0] = '\0';
	if (!noConsoleRedir) {
		AG_SetVerboseCallback(ConsoleWrite);
		AG_SetDebugCallback(ConsoleWrite);
	}

	/*
	 * Set application-global keyboard shortcuts.
	 * Map F7 and Ctrl-Shift-D to Debugger.
	 * Map F8 and Ctrl-Shift-C to Style Editor.
	 */
	AG_BindStdGlobalKeys();
#if defined(AG_DEBUG) && defined(AG_TIMERS)
	AG_BindGlobalKey(AG_KEY_F7, AG_KEYMOD_NONE, DoDebugger);
	AG_BindGlobalKey(AG_KEY_D,  AGSI_APPCMD,    DoDebugger);
#endif
	AG_BindGlobalKey(AG_KEY_F8, AG_KEYMOD_NONE, DoStyleEditor);
	AG_BindGlobalKey(AG_KEY_C,  AGSI_APPCMD,    DoStyleEditor);

	/* Check for data files in the agartest install directory. */
#if !defined(_WIN32)
	if (strcmp(DATADIR, "NONE") != 0)
		AG_ConfigAddPathS(AG_CONFIG_PATH_DATA, DATADIR);
#endif
	AG_ConfigAddPathS(AG_CONFIG_PATH_DATA, ".");

	(void)AG_ConfigLoad();
	AG_SetDefaultFont(NULL);

	if ((win = winMain = AG_WindowNew(AG_WINDOW_MAIN)) == NULL) {
		return (1);
	}
	AG_WindowSetCaptionS(win, "agartest");
	AG_WindowSetMinSize(win, 250, 100);
	
	menu = (agDriverSw) ? AG_MenuNewGlobal(0) :
                              AG_MenuNew(win, AG_MENU_HFILL);

	mi = AG_MenuNode(menu->root, ("File"), NULL);
	{
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_CLOSE_X AGSI_RST " Quit"), NULL,
		    AG_KEY_Q, AG_KEYMOD_CTRL, AGWINCLOSE(win));
	}
	mi = AG_MenuNode(menu->root, ("Edit"), NULL);
	{
		AG_MenuAction(mi,
		    _(AGSI_IDEOGRAM AGSI_GEAR AGSI_RST " Preferences"), NULL,
		    EditPreferences, NULL);
	}
	mi = AG_MenuNode(menu->root, ("Tools"), NULL);
	{
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_ARTISTS_PALETTE AGSI_RST " Style Editor"), NULL,
		    AG_KEY_F8, 0,
		    RunStyleEditor, NULL);
#if defined(AG_DEBUG) && defined(AG_TIMERS)
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_DEBUGGER AGSI_RST " GUI Debugger"), NULL,
		    AG_KEY_F7, 0,
		    RunDebugger, NULL);
#endif
		AG_MenuSeparator(mi);
#if defined(AG_TIMERS)
		AG_MenuAction(mi,
		    _(AGSI_IDEOGRAM AGSI_SURFACE_MOUNT_CHIP AGSI_RST " Drivers"), NULL,
		    RunDriversBrowser, NULL);
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_FILESYSTEM AGSI_RST " Classes"), NULL,
		    AG_KEY_S, AG_KEYMOD_CTRL_SHIFT,
		    RunClassInfo, NULL);
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_TYPOGRAPHY AGSI_RST " Fonts"), NULL,
		    AG_KEY_F, AG_KEYMOD_CTRL_SHIFT,
		    RunFontsInfo, NULL);
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_STOPWATCH AGSI_RST " Timers"), NULL,
		    AG_KEY_T, AG_KEYMOD_CTRL_SHIFT,
		    RunTimerInspector, NULL);
#endif
#if defined(AG_UNICODE)
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_CHARSETS AGSI_RST AGSI_RST " Unicode"), NULL,
		    AG_KEY_U, AG_KEYMOD_CTRL_SHIFT,
		    RunUnicodeBrowser, NULL);
#endif
	}
	mi = AG_MenuNode(menu->root, ("View"), NULL);
	{
		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_ZOOM_IN AGSI_RST " Zoom In"), NULL,
		    AG_KEY_PLUS, AG_KEYMOD_CTRL, ZoomIn, NULL);

		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_ZOOM_OUT AGSI_RST " Zoom Out"), NULL,
		    AG_KEY_MINUS, AG_KEYMOD_CTRL, ZoomOut, NULL);

		AG_MenuActionKb(mi,
		    _(AGSI_IDEOGRAM AGSI_ZOOM_RESET AGSI_RST " Zoom 1:1"), NULL,
		    AG_KEY_0, AG_KEYMOD_CTRL, ZoomReset, NULL);
#ifdef AG_DEBUG
		AG_MenuSeparator(mi);

		{
			AG_MenuItem *miKb;

			miKb = AG_MenuBool(mi, _("Debug Messages"), NULL,
			    &agDebugLvl, 0);
			miKb->key_equiv = AG_KEY_D;
			miKb->key_mod = AG_KEYMOD_CTRL;
		}
#endif
	}
	mi = AG_MenuNode(menu->root, ("Help"), NULL);
	{
		AG_MenuAction(mi,
		    _(AGSI_IDEOGRAM AGSI_AGAR_AG AGSI_RST " About Agar GUI"), NULL,
		    AG_About, NULL);
	}

	pane = AG_PaneNewHoriz(win, AG_PANE_EXPAND);

	AG_LabelNewS(pane->div[0], 0, _("Available tests: "));
	tl = AG_TlistNew(pane->div[0], AG_TLIST_EXPAND);
	AG_TlistSizeHint(tl, "<XXXXXXXXXXXXXXXXXXXXX>", 10);
	for (pTest = &testCases[0]; *pTest != NULL; pTest++) {
		AG_TlistItem *it;

		if ((*pTest)->icon[0] != '\0') {
			it = AG_TlistAdd(tl, NULL, "%s  %s", (*pTest)->icon,
			    (*pTest)->name);
		} else {
			it = AG_TlistAddS(tl, NULL, (*pTest)->name);
		}
		it->p1 = (void *)*pTest;
	}
	AG_TlistSort(tl);

	hBox = AG_BoxNewHoriz(pane->div[0], AG_BOX_HFILL | AG_BOX_HOMOGENOUS |
	                                    AG_BOX_NO_SPACING);
	{
		btnTest = AG_ButtonNew(hBox, AG_BUTTON_EXCL, _("Run ..."));
		AG_WidgetDisable(btnTest);
	}

	console = AG_ConsoleNew(pane->div[1], AG_CONSOLE_EXPAND);
	AG_SetTextColor(console, "#ddd");
	{
		AG_AgarVersion av;
		AG_DriverClass **pd;
		AG_ConsoleLine *ln;
		const char **bopt;

		AG_GetVersion(&av);
		AG_ConsoleMsgS(console, "");
		if (av.release) {
			ln = AG_ConsoleMsg(console,
			    _("Agar %d.%d.%d for %s ("
			    AGSI_LEAGUE_SPARTAN AGSI_CYAN "%s" AGSI_RST
			    ")"),
			    av.major, av.minor, av.patch, agCPU.arch,
			    av.release);
		} else {
			ln = AG_ConsoleMsg(console,
			    _("Agar %d.%d.%d for %s (beta)"),
			    av.major, av.minor, av.patch, agCPU.arch);
		}
		AG_ColorRGB(&ln->c, 200,240,240);

		AG_ConsoleMsgS(console, "");
		ln = AG_ConsoleMsgS(console, "https://libAgar.org/ | "
				             "https://patreon.com/libagar");
		AG_ColorRGB(&ln->c, 200,240,240);
		AG_ConsoleMsgS(console, "");

		ln = AG_ConsoleMsg(console,
		    _("Memory model: " AGSI_ITALIC "%s" AGSI_RST " "
		      "(%d-bit/component color & %u-bit aligned pixels)"),
		    AG_MEMORY_MODEL_NAME,
		    AG_COMPONENT_BITS, (Uint)(sizeof(AG_Pixel) << 3));
		AG_ColorFromString(&ln->c, "AntiqueWhite", NULL);

		ln = AG_ConsoleMsg(console, _("Build options: " AGSI_FONT10));
		AG_ColorFromString(&ln->c, "AntiqueWhite", NULL);
		for (bopt = &agarBuildOpts[0]; *bopt != NULL; bopt++) {
			AG_ConsoleMsgCatS(ln, *bopt);
		}
		AG_ConsoleMsgCatS(ln, AGSI_RST);

		ln = AG_ConsoleMsg(console, _("Build libs: " AGSI_FONT10));
		AG_ColorFromString(&ln->c, "AntiqueWhite", NULL);
		for (bopt = &agarBuildLibs[0]; *bopt != NULL; bopt++) {
			AG_ConsoleMsgCatS(ln, *bopt);
		}
		AG_ConsoleMsgCatS(ln, AGSI_RST);

		AG_ConsoleMsg(console, _("Available drivers:"));
		for (pd = &agDriverList[0]; *pd != NULL; pd++) {
			AG_DriverClass *dc = *pd;

			ln = AG_ConsoleMsg(console,
			    _(" -d %7s  # %s/%s, see %s(3)"),
			    dc->name,
			    agDriverTypeNames[dc->type],
			    agDriverWmTypeNames[dc->wm],
			    AGOBJECTCLASS(dc)->name);
			if (AGWIDGET(win)->drvOps == dc) {
				AG_ColorRGB_8(&ln->c, 0,255,0);
				AG_ConsoleMsgCatS(ln, _(" [current]"));
			}
		}
		AG_ConsoleMsgS(console, "");
		AG_ConsoleMsg(console,
		    _("Press " AGSI_BOLD "Ctrl-[-]" AGSI_RST
		       " and " AGSI_BOLD "Ctrl-[+]" AGSI_RST " to zoom"));
# if defined(AG_DEBUG) && defined(AG_TIMERS)
		AG_ConsoleMsg(console,
		    _("Press " AGSI_BOLD "Ctrl-Shift-D or F7" AGSI_RST " to start Debugger"));
#endif
		AG_ConsoleMsg(console,
		    _("Press " AGSI_BOLD "Ctrl-Shift-C or F8" AGSI_RST " to start Style Editor"));
		AG_ConsoleMsgS(console, "");
	}

	AG_TlistSetChangedFn(tl, SelectedTest, NULL);
	AG_TlistSetDblClickFn(tl, RunTest, "%p", tl);
	AG_TlistSetPopupFn(tl, TestPopupMenu, NULL);

	AG_SetEvent(btnTest, "button-pushed", RunTest, "%p", tl);

	statusBar = AG_StatusbarNew(win, AG_STATUSBAR_HFILL);
	status = AG_StatusbarAddLabel(statusBar, _("Please select a test to run."));
	
	AG_SetEvent(win, "window-detached", ConsoleWindowDetached, NULL);

	AG_WindowSetGeometryAligned(win, AG_WINDOW_MC, 980, 540);
	AG_WindowShow(win);

#ifdef _WIN32
	optInd++;                                 /* Skip pathname argument */
#endif
	if (optInd == argc &&
	    AG_GetBool(agConfig,"initial-run") == 1) {
#ifdef AUTORUN_WIDGETS
		AG_Event ev;

		AG_TlistSelectPtr(tl, (void *)&widgetsTest);
		AG_EventArgs(&ev, "%p,%p", tl, win);
		RunTest(&ev);
#endif
		AG_SetBool(agConfig,"initial-run",0);
		if (AG_ConfigSave() == -1)
			AG_Verbose("AG_ConfigSave: %s; ignoring", AG_GetError());
	}
	for (i = optInd; i < argc; i++) {
		AG_Event ev;

		for (pTest = &testCases[0]; *pTest != NULL; pTest++) {
			if (AG_Strcasecmp((*pTest)->name, argv[i]) == 0)
				break;
		}
		if (*pTest == NULL) {
			AG_ConsoleMsg(console, _("No such test: %s"), argv[i]);
			continue;
		}
		AG_TlistSelectPtr(tl, (void *)(*pTest));
		AG_EventArgs(&ev, "%p,%p", tl, win);
		RunTest(&ev);
	}
	if (AG_ConfigSave() == -1)
		AG_Verbose("ConfigSave: %s\n", AG_GetError());

	AG_EventLoop();
	AG_DestroyGraphics();
	AG_Destroy();
	return (0);
fail:
	printf("Agar initialization failed: %s\n", AG_GetError());
	if (AG_GetErrorCode() == AG_ENOENT) {
		AG_DriverClass **pd;

		printf("Available drivers include:\n");
		for (pd = &agDriverList[0]; *pd != NULL; pd++) {
			AG_DriverClass *dc = *pd;

			printf(_("    -d %7s          # %s, %s (see %s(3))\n"),
			    dc->name,
			    agDriverTypeNames[dc->type],
			    agDriverWmTypeNames[dc->wm],
			    AGOBJECTCLASS(dc)->name);
		}
	}
	return (1);
}
