#include <pspkernel.h>
#include <pspdebug.h>
#include <pspctrl.h>
#include <pspdisplay.h>
#include <string.h>
#include <pspiofilemgr.h>
#include <stdio.h>
#include "pikey.h"
#include "apihook.h"
#include "blit.h"
#include "mapkey.h"

PSP_MODULE_INFO("piKey", 0x1000, 1, 1);

/*****************************************************************************/
/* Write debug info to SIO?                                                  */
/*                                                                           */
/* 0 : off                                                                   */
/* 1 : major events                                                          */
/* 2 : debug events                                                          */
/* 3 : extra debug                                                           */
/*****************************************************************************/
#define DEBUG_OUTPUT 1

/*****************************************************************************/
/* Internal structs                                                          */
/*****************************************************************************/
typedef struct input_info {
	struct input_info *prev;
	struct input_info *next;
	SceUID             moduleID;
	char               moduleName[64];
} INPUT_INFO;

typedef struct output_info {
	struct output_info *prev;
	struct output_info *next;
	SceUID              moduleID;
	char                moduleName[64];
} OUTPUT_INFO;

void DumpMem(void *addr,unsigned size,int num);

typedef int (*DRIVERINFOFUNC)(DRIVERINFO *);

/*****************************************************************************/
/* Internal vars                                                             */
/*****************************************************************************/
int terminate = 0;

int numInputs;
int numOutputs;

SceUID pikeythread;

INPUT_INFO  *inputs;
OUTPUT_INFO *outputs;

#define QUEUE_SIZE 256
#define MAX_PATH_SIZE 256

/*****************************************************************************/
/* Cyclic buffer of max-size QUEUE_SIZE for input chars.                     */
/*****************************************************************************/
wchar inBuffer[QUEUE_SIZE];
int queueInputPos;
int queueOutputPos;
int queueSize;
SceUID queueSema;
SceUID gExclusiveThread;

/*****************************************************************************/
/* Keyboard info display callbacks                                           */
/*****************************************************************************/
#define MAX_DISPLAY_CALLBACKS 10
SceUID gDisplayCallbacks[MAX_DISPLAY_CALLBACKS];
int    gNumDisplayCallbacks = 0;
SceUID callbackSema;

/*****************************************************************************/
/* Status indication                                                         */
/*****************************************************************************/
// num of vblanks to display status text for. (3 secs)
#define STATUS_DISPLAY_TIME 180
#define SCREEN_TEXT_WIDTH  80

static char centredText[SCREEN_TEXT_WIDTH + 1];
int gStatusCycles = 0;
int gStatusText   = 0;
int statusMessages = 1;
int sioDebug = 0;
int disabledIn150 = 0;

/*****************************************************************************/
/* VSH mode?                                                                 */
/*****************************************************************************/
int gvshmode = 0;

/*****************************************************************************/
/* Keypress mode vars                                                        */
/*****************************************************************************/
int  gOldKeyMode = PIKEY_KEYMODE_KEYPRESS;
int  gKeyMode = PIKEY_KEYMODE_KEYPRESS;
char gPressMap[KEY_MAX];
char gHoldMap[KEY_MAX];
int  numPressed = 0;
int  altHeld = 0;
int  ctrlHeld = 0;
int  shiftHeld = 0;

/*****************************************************************************/
/* Key repeat metrics - measured in vblanks                                  */
/*                                                                           */
/* REPEAT_TIME: time to first repeat                                         */
/* REPEAT_PERIOD: time between repeats once started.                         */
/*****************************************************************************/
#define REPEAT_TIME   30
#define REPEAT_PERIOD 10

// measured in microseconds - time to simulate a keyhold event when sent a
// character in keypress mode.
#define FAKE_KEYPRESS_HOLD 80000

// length of a vblank frame (60hz) in microseconds.
#define FRAME_LENGTH_USECS 16667

/*****************************************************************************/
/* ESCAPE key timeout hack                                                   */
/*****************************************************************************/
int gEscTimer = -1;
// number of vblanks to wait for further chars in a vt100 escape sequence
#define ESC_TIMEOUT 6

/*****************************************************************************/
/* Config file control vars.                                                 */
/*****************************************************************************/
#define MAX_CONFIGS 10
char * configData[MAX_CONFIGS];
char * configOffset[MAX_CONFIGS];
int    numConfigs = 0;

char cfgtoken[64];
char cfgvalue[128];

/*****************************************************************************/
/* Disabled plugins list                                                     */
/*****************************************************************************/
#define MAX_DISABLED_PLUGINS 10
char disabledPlugins[MAX_DISABLED_PLUGINS][128];
int  numDisabledPlugins = 0;


/*****************************************************************************/
/*                                                                           */
/* Code starts here                                                          */
/*                                                                           */
/*****************************************************************************/
void sioPrint(const char *str)
{
#if DEBUG_OUTPUT!=0
	if (sioDebug)
	{
		while (*str)
		{
			pspDebugSioPutchar(*str);
			str++;
		}
	}
#endif
}

void sioPrintHex(char ch)
{
#if DEBUG_OUTPUT!=0
	if (sioDebug)
	{
		static char * hexmap="0123456789ABCDEF";
		pspDebugSioPutchar( hexmap[ (ch >> 4) & 0x0F ] );
		pspDebugSioPutchar( hexmap[ (ch & 0x0F) ] );
	}
#endif
}

void sioPrintWord(u32 data)
{
#if DEBUG_OUTPUT!=0
	if (sioDebug)
	{
		sioPrintHex( (data >> 24) & 0x000000FFL );
		sioPrintHex( (data >> 16) & 0x000000FFL );
		sioPrintHex( (data >>  8) & 0x000000FFL );
		sioPrintHex( data & 0x000000FFL );
	}
#endif
}

int waitForModule(const char * modname)
{
	int cycles = 0;

#if DEBUG_OUTPUT>=2
	sioPrint("Wait for module ");
	sioPrint(modname);
	sioPrint(": ");
#endif

	while (sceKernelFindModuleByName(modname) == NULL)
	{
#if DEBUG_OUTPUT>=2
		pspDebugSioPutchar('.');
#endif
		if (cycles++ > 10)
		{
#if DEBUG_OUTPUT>=2
			sioPrint("TIMEOUT.\r\n");
#endif
			// give up waiting after 5 secs
			return 0;
		}

		sceKernelDelayThread(500000);
	}

#if DEBUG_OUTPUT>=2
	sioPrint("OK.\r\n");
#endif
	return 1;
}

static struct SceLibraryEntryTable *_libsFindLibrary(SceUID uid, const char *library)
{
	struct SceLibraryEntryTable *entry;
	SceModule *pMod;
	void *entTab;
	int entLen;

	pMod = sceKernelFindModuleByUID(uid);
	if(pMod != NULL)
	{
		int i = 0;

		entTab = pMod->ent_top;
		entLen = pMod->ent_size;
		while(i < entLen)
		{
			entry = (struct SceLibraryEntryTable *) (entTab + i);

			if((entry->libname) && (strcmp(entry->libname, library) == 0))
			{
				return entry;
			}
			else if(!entry->libname && !library)
			{
				return entry;
			}

			i += (entry->len * 4);
		}
	}

	return NULL;
}

static void* libsFindExportAddrByNid(SceUID uid, const char *library, u32 nid)
{
	u32 *addr = NULL;
	SceLibraryEntryTable *entry = _libsFindLibrary(uid, library);

	if(entry)
	{
		int count;
		int total;
		unsigned int *vars;

		total = entry->stubcount + entry->vstubcount;
		vars = entry->entrytable;

		if(entry->stubcount > 0)
		{
			for(count = 0; count < entry->stubcount; count++)
			{
				if(vars[count] == nid)
				{
					return (void*)(*((void**)(&vars[count+total])));
				}
			}
		}
	}

	return addr;
}

SceUID load_module(const char *path, int flags, int type) {
	SceKernelLMOption option;
	SceUID mpid;

	/* If the type is 0, then load the module in the kernel partition, otherwise load it
	   in the user partition. */
	if (type == 0) mpid = 1;
	else mpid = 2;

	memset(&option, 0, sizeof(option));
	option.size = sizeof(option);
	option.mpidtext = mpid;
	option.mpiddata = mpid;
	option.position = 0;
	option.access = 1;

	return sceKernelLoadModule(path, flags, type > 0 ? &option : NULL);
}

void centre_string(const char *text)
{
	centredText[SCREEN_TEXT_WIDTH] = '\0';
	memset(centredText, ' ', SCREEN_TEXT_WIDTH);

	int length = strlen(text);
	int offset;

	// Cope with over-length string
	if (length > SCREEN_TEXT_WIDTH)
	{
		length = SCREEN_TEXT_WIDTH;
		offset = 0;
	}
	else
	{
		// Note : calculation relies on max width being even to avoid overflow
		offset = (SCREEN_TEXT_WIDTH - length) / 2;
	}

	int ii;
	for (ii = 0; ii < length; ii++)
	{
		centredText[offset++] = text[ii];
	}
}

void displayStatusText(const char *text)
{
	if (statusMessages)
	{
		centre_string(text);
		gStatusCycles = 0;
		gStatusText   = 1;

#if DEBUG_OUTPUT>=2
		sioPrint("Show status text: ");
		sioPrint(text);
		sioPrint("\r\n");
#endif
	}
}

void InitVars(void)
{
	// empty queues, driver counts
	numInputs  = 0;
	numOutputs = 0;

	inputs = NULL;
	outputs = NULL;

	queueInputPos = 0;
	queueOutputPos = 0;
	queueSize = 0;

	ctrlHeld = 0;
	altHeld = 0;
	shiftHeld = 0;

	gExclusiveThread = 0;

	queueSema    = sceKernelCreateSema("pikey_q",     0, 1, 1, NULL);
	callbackSema = sceKernelCreateSema("pikey_calls", 0, 1, 1, NULL);

	gNumDisplayCallbacks = 0;

	gStatusText = 0;
	gStatusCycles = 0;

	// default to keypress mode, no keys currently pressed
  gKeyMode = PIKEY_KEYMODE_KEYPRESS;
	memset(gPressMap, 0, sizeof(gPressMap));
	numPressed = 0;

  numConfigs = 0;
	int ii;
	for (ii = 0; ii < MAX_CONFIGS; ii++)
	{
		configData[ii] = NULL;
		configOffset[ii] = NULL;
	}

	initApiHooks();
}

/*****************************************************************************/
/* Simple malloc clone.  The block ID is stored just before the returned 		 */
/* pointer.  																																 */
/*****************************************************************************/
void *myMalloc(size_t size)
{
	SceUID memID = sceKernelAllocPartitionMemory(1, "", 0, size + 4, NULL);
	if (memID < 0)
	{
#if DEBUG_OUTPUT>=2
		sioPrint("sKAPM failed: ");
		sioPrintWord(memID);
		sioPrint("\r\n");
#endif
		return NULL;
	}

	unsigned int * lptr = sceKernelGetBlockHeadAddr(memID);
	*lptr = memID;

#if DEBUG_OUTPUT>=3
		sioPrint("alloced blockID: ");
		sioPrintWord(memID);
		sioPrint(" size ");
		sioPrintWord(size);
		sioPrint("\r\n");
#endif

	return (lptr+1);
}

/*****************************************************************************/
/* Corresponding Free func for myMalloc                                      */
/*****************************************************************************/
void myFree(void *blockaddr)
{
  SceUID blockid = *(((SceUID*)blockaddr) - 1);

#if DEBUG_OUTPUT>=3
		sioPrint("Free blockID: ");
		sioPrintWord(blockid);
		sioPrint("\r\n");
#endif

	int rc = sceKernelFreePartitionMemory(blockid);
	if (rc < 0)
	{
		sioPrint("Error freeing block at ");
		sioPrintWord((int)blockaddr);
		sioPrint(" err=");
		sioPrintWord(rc);
		sioPrint("\r\n");
	}
}

SceIoDirent dirent;

void LoadDrivers(char *path, int isInput)
{
	int status;

#if DEBUG_OUTPUT>=2
	sioPrint("Load drivers from ");
	sioPrint(path);
	sioPrint("\r\n");
#endif

	// iterate through "path/", loading all the prxs
	memset(&dirent, 0, sizeof(SceIoDirent));

	SceUID lid = sceIoDopen(path);

	if (lid >= 0)
	{
		int lrc = 0;

		do {
			lrc = sceIoDread(lid, &dirent);
			if (lrc > 0) {
				if (strstr(dirent.d_name, ".prx") || strstr(dirent.d_name, ".PRX")) {
					char namebuff[MAX_PATH_SIZE];
					strncpy(namebuff, path, MAX_PATH_SIZE);
					strcat(namebuff, "/");
					strcat(namebuff, dirent.d_name);

#if DEBUG_OUTPUT>=2
					sioPrint(namebuff);
					sioPrint(":\r\n");
#endif

					int disabled = 0;
					int ii;
					for (ii = 0; ii < numDisabledPlugins; ii++)
					{
#if DEBUG_OUTPUT>=3
            sioPrint("Check : '");
						sioPrint(disabledPlugins[ii]);
						sioPrint("'\r\n");
#endif

						if (strcmp(dirent.d_name, disabledPlugins[ii]) == 0)
						{
#if DEBUG_OUTPUT>=1
							sioPrint("Skip ");
							sioPrint(dirent.d_name);
							sioPrint(" - disabled module\r\n");
#endif
							disabled = 1;
						}
					}

					if (disabled)
					{
						continue;
					}

					SceUID modid = load_module(namebuff, 0, 0);
					if (modid < 0)
					{
#if DEBUG_OUTPUT>=1
						sioPrint("Load module failed: 0x");
						sioPrintWord(modid);
						sioPrint("\r\n");
#endif
					}

					int done = 0;
					while (!done)
					{
						int lrc2 = sceKernelStartModule(modid, 0, NULL, &status, NULL);
						if (lrc2 < 0)
						{
#if DEBUG_OUTPUT>=1
							sioPrint("Start module failed: 0x");
							sioPrintWord(lrc2);
#endif

							if (lrc2 == 0x8002013CL)
							{
#if DEBUG_OUTPUT>=1
								sioPrint("...sleep and retry...\r\n");
#endif
								sceKernelDelayThread(500000);
							}
							else
							{
#if DEBUG_OUTPUT>=1
								sioPrint("...Give up.\r\n");
#endif
								done = 1;
							}
						}
						else
						{
#if DEBUG_OUTPUT>=2
							sioPrint("Started.\r\n");
#endif
							done = 1;

							// Now we started the module successfully, call its driverinfo func.
							DRIVERINFOFUNC info_func = NULL;
							DRIVERINFO ldriverinfo;
							memset(&ldriverinfo, 0, sizeof(ldriverinfo));

							SceKernelModuleInfo lmodinfo;
							memset(&lmodinfo, 0, sizeof(lmodinfo));
							lmodinfo.size = sizeof(lmodinfo);

							int lmodinforc = sceKernelQueryModuleInfo(modid, &lmodinfo);
              if (lmodinforc == 0)
							{
								info_func = libsFindExportAddrByNid(modid, lmodinfo.name, 0x79EC1E42);

#if DEBUG_OUTPUT>=1
								if (info_func == NULL)
								{
									sioPrint("Failed to get driver info export.\r\n");
									sioPrint("Was using name: '");
									sioPrint(lmodinfo.name);
									sioPrint("'\r\n");
								}
#endif

								ldriverinfo.apiVersion = PIKEY_THIS_VERSION;
							}
#if DEBUG_OUTPUT>=1
							else
							{
								sioPrint("Failed to get module info. RC=");
								sioPrintWord(lmodinforc);
								sioPrint("\r\n");
							}
#endif

							if ((info_func == NULL) || (info_func(&ldriverinfo) != PIKEY_SUCCESS))
							{
								// XXX should unload the driver here.
#if DEBUG_OUTPUT>=1
								sioPrint("Would unload plugin here.\r\n");
#endif
							}
							else
							{
#if DEBUG_OUTPUT>=1
								// output some info about the driver
								sioPrint("Successfully loaded driver '");
								sioPrint(ldriverinfo.driverName);
								sioPrint("' version ");
								sioPrint(ldriverinfo.driverVersion);
								sioPrint("\r\n");
#endif
							}

              /***************************************************************/
              /* Save some info in our linked list of driver info.           */
              /***************************************************************/
							if (isInput)
							{
								INPUT_INFO *linfo = (INPUT_INFO*)myMalloc(sizeof(INPUT_INFO));
								if (!linfo)
								{
									sioPrint("malloc failed\r\n");
								}
								else
								{
									linfo->next = NULL;
									if (inputs == NULL)
									{
										linfo->prev = NULL;
										inputs = linfo;
									}
									else
									{
										INPUT_INFO *lptr = inputs;
										while (lptr->next)
										{
											lptr = lptr->next;
										}
										linfo->prev = lptr;
										lptr->next = linfo;
									}

									linfo->moduleID = modid;
									strcpy(linfo->moduleName, ldriverinfo.driverName);
								}

								numInputs++;
							}
							else
							{
								OUTPUT_INFO *linfo = (OUTPUT_INFO*)myMalloc(sizeof(OUTPUT_INFO));
								if (!linfo)
								{
									sioPrint("malloc failed\r\n");
								}
								else
								{
									linfo->next = NULL;
									if (outputs == NULL)
									{
										linfo->prev = NULL;
										outputs = linfo;
									}
									else
									{
										OUTPUT_INFO *lptr = outputs;
										while (lptr->next)
										{
											lptr = lptr->next;
										}
										linfo->prev = lptr;
										lptr->next = linfo;
									}

									linfo->moduleID = modid;
									strcpy(linfo->moduleName, ldriverinfo.driverName);
								}
								numOutputs++;
							}

						}
					}
				}
			}
		}
		while (lrc > 0);
		sceIoDclose(lid);
	}
}

#if DEBUG_OUTPUT>=1
void DumpMem(void *addr,unsigned size,int num)//DELME
{
	char buf[270];
	SceUID fd;
	sprintf(buf,"ms0:/%d_%08X_%08X.bin",num,(int)addr,size);
	fd = sceIoOpen(buf,PSP_O_WRONLY|PSP_O_CREAT|PSP_O_TRUNC,0777);
	if(fd < 0)
        return;
	sceIoWrite(fd,addr,size);
	sceIoClose(fd);
}
#endif

int main_thread(SceSize args, void *argp)
{
	unsigned int disableHotkey = 0;
	unsigned int oskstate = 0;

	// Read the config file for user-defined control map
	int cfg = configOpen("ms0:/seplugins/pikey/pikeyconfig.txt");
	if (cfg >= 0)
	{
		while (configRead(cfg, cfgtoken, cfgvalue))
		{
			if (strcmp(cfgtoken, "SIO DEBUG") == 0)
			{
				sioDebug = ((cfgvalue[0] == 'Y') || (cfgvalue[0] == 'y'));
			}
			else if (strcmp(cfgtoken, "STATUS MESSAGES") == 0)
			{
				statusMessages = ((cfgvalue[0] == 'Y') || (cfgvalue[0] == 'y'));
			}
			else if (strcmp(cfgtoken, "DISABLED IN 150") == 0)
			{
				disabledIn150 = ((cfgvalue[0] == 'Y') || (cfgvalue[0] == 'y'));
			}
			else if (strcmp(cfgtoken, "DISABLED PLUGINS") == 0)
			{
				// split the CSV list, save in disabledPlugins[]
				char *lptr = cfgvalue;
				char *ldestptr = disabledPlugins[0];
				numDisabledPlugins = 0;
				while (*lptr != '\0')
				{
					if (*lptr == ' ')
					{
						// skip spaces
					}
					else if (*lptr == ',')
					{
						*ldestptr = '\0';

						if (numDisabledPlugins++ > MAX_DISABLED_PLUGINS)
						{
							// too many disabled plugins
							break;
						}
						else
						{
							ldestptr = disabledPlugins[numDisabledPlugins];
						}
					}
					else
					{
						*ldestptr++ = *lptr;
					}

					lptr++;
				}

				// if we read any data, then adjust the disabledplugins count
				// (since we didn't increment it at the end of the string, just the
				// last comma)
				if (ldestptr != disabledPlugins[0])
				{
					numDisabledPlugins ++;
				}
			}
			else if (strcmp(cfgtoken, "DISABLE HOTKEY") == 0)
			{
				if ((cfgvalue[0] == '0') &&
				 		((cfgvalue[1] == 'x') ||
						 (cfgvalue[1] == 'X')))
				{
					unsigned int lhex = 0;
					char *lptr = &(cfgvalue[2]);
					while ((*lptr != '\0') && (*lptr != ' '))
					{
						// This is a hex number, decode it.
						char lhexdigit = (*lptr) - '0';
						if (lhexdigit > 9)
						{
							lhexdigit -= ('A' - '9' - 1);
						}
						if (lhexdigit > 15)
						{
							lhexdigit -= ('a' - 'A');
						}

						if (lhexdigit < 0)
						{
							lhexdigit = 0;
						}

						lhex <<= 4;
						lhex += lhexdigit;

						lptr++;
					}

					disableHotkey = lhex;
				}
			}
		}

		configClose(cfg);
	}

	// If we're in v1.50 firmware, and we're disabled for this mode, then exit.
	if (disabledIn150)
	{
		if (sceKernelDevkitVersion() == 0x01050001L)
		{
			sceKernelExitDeleteThread(0);
		}
	}

	// If there's a disable hotkey, and it's pressed, then exit.
	if (disableHotkey != 0)
	{
		SceCtrlData lpad;

		sceCtrlPeekBufferPositive(&lpad, 1);

		if ((lpad.Buttons & disableHotkey) == disableHotkey)
		{
			sceKernelExitDeleteThread(0);
		}
	}

	if (sioDebug)
	{
		pspDebugSioInit();
		sioPrint("\r\n\r\n");

#if DEBUG_OUTPUT>=2
    pspDebugSioInstallKprintf();
    pspDebugSioEnableKprintf();
#endif

		sioPrint("  -----------------------\r\n");
		sioPrint("  -- piKey is starting --\r\n");
		sioPrint("  -----------------------\r\n");

#if DEBUG_OUTPUT >= 3
   	int ii;
   	for (ii = 1; ii <= 6; ii++)
   	{
   		sioPrint("\r\nFreemem: ");
   		sioPrintHex(ii);
   		sioPrint(" = ");
   		SceSize mem = sceKernelPartitionMaxFreeMemSize(ii);
   		sioPrintWord(mem);
   	}
#endif
	}

	InitVars();

	gvshmode = waitForModule("sceVshBridge_Driver");

	if (gvshmode)
	{
		sioPrint("Detected VSH mode\r\n");

		// sleep here to avoid corrupting the startup animation
		sceKernelDelayThread(4 * 1000 * 1000);
	}
	else
	{
		sioPrint("Detected GAME mode\r\n");
	}


	LoadDrivers("ms0:/seplugins/pikey/inputdrivers", 1);
	LoadDrivers("ms0:/seplugins/pikey/outputdrivers", 0);

	sioPrint("--- All plugins loaded ---\r\n");

	displayStatusText("piKey is now ACTIVE");

	while (!terminate)
	{
		sceKernelDelayThread(100);  // Yields in a way that waitvblank does not
		                            // Without this tiny sleep, we won't exit
		                            // cleanly during module tidy-up.
		sceDisplayWaitVblankStart();

		// If we have something to display, display it
		if (gStatusText)
		{
			font_init();

			if (++gStatusCycles > STATUS_DISPLAY_TIME)
			{
				gStatusCycles = 0;
				gStatusText   = 0;
				memset(centredText, ' ', sizeof(centredText));
				blit_string(0, 0, centredText, 1);
			}
			else
			{
				blit_string(0, 0, centredText, 1);
			}
		}

		// check for held keys.  We only need to do this for stream mode, with
		// some keys held.
		if ((gKeyMode == PIKEY_KEYMODE_VT100STREAM) &&
				(numPressed > 0))
		{
			int ii;
			for (ii=0; ii < KEY_MAX; ii++)
			{
				if (gPressMap[ii])
				{
					if ((gHoldMap[ii])++ > REPEAT_TIME + REPEAT_PERIOD)
					{
						gHoldMap[ii] = REPEAT_TIME;

						unsigned char str[16];
						if (mapScanToVT100(ii, ctrlHeld, shiftHeld, altHeld, str))
						{
							unsigned char *lptr = str;
							while (*lptr)
							{
								putNextChar(*lptr);
								lptr++;
							}
						}
					}
				}
			}
		}

		// Wait until we are activated with the NUM lock key.
		if (gPressMap[KEY_NUMLOCK])
		{
			// check if JUST pressed
			if (!(oskstate & 32))
			{
				oskstate |= 32; // NumLock pressed

				oskstate ^= 4;
				if (oskstate & 4)
				{
					sioPrint("NUM lock is ON\r\n");
					displayStatusText("NUM lock is ON");
				}
				else
				{
					sioPrint("NUM lock is OFF\r\n");
					displayStatusText("NUM lock is OFF");
				}
			}
		}
		else
			oskstate &= ~32; // NumLock not pressed

		// Wait until we are activated with the scroll lock key.
		if (gPressMap[KEY_SCROLLLOCK])
		{
			// check if JUST pressed
			if (!(oskstate & 16))
			{
				oskstate |= 16; // ScrollLock pressed

				oskstate ^= 2;
				if (oskstate & 2)
				{
					sioPrint("SCROLL lock is ON\r\n");
					displayStatusText("SCROLL lock is ON");
				}
				else
				{
					sioPrint("SCROLL lock is OFF\r\n");
					displayStatusText("SCROLL lock is OFF");
				}
			}
		}
		else
			oskstate &= ~16; // ScrollLock not pressed

		// Wait until we are activated with the CAPS lock key.
		if (gPressMap[KEY_CAPSLOCK])
		{
			// check if JUST pressed
			if (!(oskstate & 8))
			{
				oskstate |= 8; // CapsLock pressed

				oskstate ^= 1;
				if (oskstate & 1)
				{
					sioPrint("CAPS lock is ON\r\n");
					displayStatusText("CAPS lock is ON");
				}
				else
				{
					sioPrint("CAPS lock is OFF\r\n");
					displayStatusText("CAPS lock is OFF");
				}
			}
		}
		else
			oskstate &= ~8; // CapsLock not pressed

		// if we have an ESC charinput, in keypress mode, check whether we
		// need to time it out into a simple ESC.
		if ((gKeyMode == PIKEY_KEYMODE_KEYPRESS) &&
				(gEscTimer != -1))
		{
			gEscTimer++;

			if ((gEscTimer > ESC_TIMEOUT) &&
				  !(gPressMap[KEY_ESC]))
			{
				// we have a lone escape key.  Press it...
				resetMapkey();
				sendKeyPress(KEY_ESC, 1, 0, 0, 0);
			}
			else if (gEscTimer > (ESC_TIMEOUT +
						                (FAKE_KEYPRESS_HOLD / FRAME_LENGTH_USECS)))
			{
				// ... and release it after another timeout
				sendKeyPress(KEY_ESC, 0, 0, 0, 0);

				gEscTimer = -1;
			}
		}
	}

	sioPrint("--- Exiting piKey ---\r\n");

	sceKernelExitDeleteThread(0);

	return 0;
}

int main(void) {
	int rc = 0;

  /***************************************************************************/
	/* Create main thread.                                                     */
	/***************************************************************************/
	pikeythread = sceKernelCreateThread("pikey", main_thread, 16, 0x1000, 0, NULL);
	if (pikeythread >= 0)
	{
		rc = sceKernelStartThread(pikeythread, 0, NULL);
	 	if (rc < 0)
		{
			return -2;
		}
	}
	else
	{
		return -1;
	}

	return 0;
}

void throwAwayInput(int numchars)
{
	while (numchars--)
	{
		if (numCharsAvailable() > 0)  getNextChar();
	}
}

void putNextCharW(wchar inChar) {
	if (gKeyMode == PIKEY_KEYMODE_VT100STREAM)
	{
		// block until queue is not full
		int lfull = 1;

		while (lfull) {
			sceKernelWaitSema(queueSema, 1, 0);
			if (queueSize != QUEUE_SIZE) lfull = 0;
			else {
				sceKernelSignalSema(queueSema, 1);
				sceKernelDelayThread(5000);
			}
		}

		// add to Q
		inBuffer[queueInputPos] = inChar;
		queueInputPos = (queueInputPos + 1) % QUEUE_SIZE;
		queueSize ++;

		// release Q sema
		sceKernelSignalSema(queueSema, 1);
	}
	else
	{
		// for keypress mode, we need to simulate a keypress and release
		int shift, ctrl, alt;

		int lscan = mapVT100toScan(inChar, &ctrl, &alt, &shift);

#if DEBUG_OUTPUT>=3
		sioPrint("vt100: ");
		sioPrintHex(inChar);
		sioPrint("=> ");
		sioPrintWord(lscan);
		sioPrint(" : ");
		sioPrintHex((ctrl << 2) | (alt << 1) | (shift));
		sioPrint("\r\n");
#endif

		// special hack to ESC chars - timeout a single ESC after a short time,
		// if it isn't followed by any other chars.
		if (inChar == 0x1b)
		{
			gEscTimer = 0;
		}

		if (lscan != -1)
		{
			gEscTimer = -1;
			sendKeyPress(lscan, 1, shift, ctrl, alt);
			sceKernelDelayThread(FAKE_KEYPRESS_HOLD);
			sendKeyPress(lscan, 0, shift, ctrl, alt);
		}
	}
}

void putNextChar(char inChar) {
	// we do a conversion to wchar then call the putwchar func.
	// XXX : for now assume a very simple conversion
	putNextCharW((wchar)inChar);
}

int numCharsAvailable(void) {
	int lreturn = 0;

	// only valid in stream mode
	if (gKeyMode == PIKEY_KEYMODE_VT100STREAM)
	{
   	sceKernelWaitSema(queueSema, 1, 0);
   	lreturn = queueSize;

   	// return 0 if there's an exclusive thread and we're not it
   	if (gExclusiveThread != 0)
   	{
   		if (sceKernelGetThreadId() != gExclusiveThread)
   		{
   			lreturn = 0;
   		}
   	}

   	sceKernelSignalSema(queueSema, 1);
	}
	else
	{
		lreturn = PIKEY_ERROR_WRONG_MODE;
	}

	return(lreturn);
}

/*****************************************************************************/
/* Returns PIKEY_SUCCESS if there are n chars available, copies them to      */
/* buffer                                                                    */
/*****************************************************************************/
int peekNChars(int n, wchar *buffer) {
	int ii;

	// only valid in stream mode
	if (gKeyMode != PIKEY_KEYMODE_VT100STREAM)
	{
		return PIKEY_ERROR_WRONG_MODE;
	}

	sceKernelWaitSema(queueSema, 1, 0);
	int lqueueSize = queueSize;

	// override queue size if there's an exclusive thread and we're not it
	if (gExclusiveThread != 0)
	{
		if (sceKernelGetThreadId() != gExclusiveThread)
		{
			lqueueSize = 0;
		}
	}

	if (lqueueSize < n)
	{
		sceKernelSignalSema(queueSema, 1);
		return PIKEY_ERROR_NOT_ENOUGH_CHARS;
	}

	// copy to buffer
	for (ii = 0; ii < n; ii++)
	{
		buffer[ii] = inBuffer[(queueOutputPos + ii) % QUEUE_SIZE];
	}

	sceKernelSignalSema(queueSema, 1);
	return(PIKEY_SUCCESS);
}

void _flushBuffer(void)
{
	queueInputPos = 0;
	queueOutputPos = 0;
	queueSize = 0;
	resetMapkey();
	gEscTimer = -1;
}

void flushBuffer(void) {
	sceKernelWaitSema(queueSema, 1, 0);


	// do nothing if there's an exclusive thread and we're not it
	if ((gExclusiveThread == 0)	|| (sceKernelGetThreadId() == gExclusiveThread))
	{
		_flushBuffer();
	}

	sceKernelSignalSema(queueSema, 1);
}

int requestExclusive(void)
{
  SceUID lthid = sceKernelGetThreadId();
	int lreturn;

	// semaphore this using the standard queue semaphore
	sceKernelWaitSema(queueSema, 1, 0);

  /***************************************************************************/
	/* Return success if we already have exclusivity.                          */
	/***************************************************************************/
	if (gExclusiveThread == lthid)
	{
		lreturn = 1;
	}
  /***************************************************************************/
	/* Return failure if someone else already has exclusivity                  */
	/***************************************************************************/
	else if (gExclusiveThread != 0)
	{
		lreturn = 0;
	}
	else
	{
		gExclusiveThread = lthid;
		gOldKeyMode = gKeyMode;

	  lreturn = 1;
	}

	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

void releaseExclusive(void)
{
  SceUID lthid = sceKernelGetThreadId();

	sceKernelWaitSema(queueSema, 1, 0);

	// Can only release exclusive if we are the exclusive thread
	if (lthid == gExclusiveThread)
	{
		gExclusiveThread = 0;

		_setMode(gOldKeyMode);
	}

	sceKernelSignalSema(queueSema, 1);
}

// mode: 0: keypress  1: stream
int setMode(int mode)
{
	int lreturn = PIKEY_SUCCESS;

	// change mode only if exclusive
  SceUID lthid = sceKernelGetThreadId();

	sceKernelWaitSema(queueSema, 1, 0);

	// Can only change mode if we are the exclusive thread
	if (lthid == gExclusiveThread)
	{
		_setMode(mode);
	}
	else
	{
		lreturn = PIKEY_ERROR_NOT_EXCLUSIVE;
	}

	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

void _setMode(int mode)
{
	gKeyMode = mode;

	_flushKeyPresses();
	_flushBuffer();
}

int	getMode(void)
{
	return gKeyMode;
}

int isKeyPressed(int keycode)
{
	sceKernelWaitSema(queueSema, 1, 0);

	int lreturn = gPressMap[keycode];

	// return 0 if there's an exclusive thread and we're not it
	if (gExclusiveThread != 0)
	{
		if (sceKernelGetThreadId() != gExclusiveThread)
		{
			lreturn = 0;
		}
	}

	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

void getMetaKeys(int *alt, int *ctrl, int *shift)
{
	sceKernelWaitSema(queueSema, 1, 0);

	// meaningless when not in keypress mode.
	if (gKeyMode == PIKEY_KEYMODE_KEYPRESS)
	{
		*alt = altHeld;
		*ctrl = ctrlHeld;
		*shift = shiftHeld;
	}
	else
	{
		*alt = 0;
		*ctrl = 0;
		*shift = 0;
	}

	sceKernelSignalSema(queueSema, 1);
}

void _flushKeyPresses()
{
	memset(gPressMap, 0, sizeof(gPressMap));
	memset(gHoldMap, 0, sizeof(gHoldMap));
	numPressed = 0;
	gEscTimer = -1;
	resetMapkey();
}

void flushKeyPresses()
{
	sceKernelWaitSema(queueSema, 1, 0);

	// don't allow flush if there's an exclusive thread and we're not it
	if ((gExclusiveThread == 0) ||
		  (sceKernelGetThreadId() == gExclusiveThread))
	{
		_flushKeyPresses();
	}

	sceKernelSignalSema(queueSema, 1);
}

int numKeysPressed()
{
	int lreturn = 0;

 	sceKernelWaitSema(queueSema, 1, 0);

	// don't allow read if there's an exclusive thread and we're not it
	if ((gExclusiveThread == 0) ||
		  (sceKernelGetThreadId() == gExclusiveThread))
	{
		lreturn = numPressed;
	}

 	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

void sendKeyPress(int keycode, int pressed, int shift, int ctrl, int alt)
{
	if (pressed)
	{
		if (!gPressMap[keycode])
		{
			// only if wasn't already pressed (repeat issue)
			gHoldMap[keycode] = 0;
			gPressMap[keycode] = 1;
			numPressed ++;
		}

#if DEBUG_OUTPUT >= 3
    sioPrint("Keypress: ");
  	sioPrintWord(keycode);
  	sioPrint(" ");
  	sioPrintWord((shift << 2) | (ctrl << 1) | alt);
  	sioPrint("\r\n");
#endif

		if (gKeyMode != PIKEY_KEYMODE_KEYPRESS)
		{
			// We're in stream mode, so we need to be a bit fancier.
			// If this is a press event, we just add the relevant char to the
			// buffer.
			// XXX : this needs a mapping to allow for capslock etc.
 			unsigned char str[16];
 			if (mapScanToVT100(keycode, ctrlHeld, shiftHeld, altHeld, str))
 			{
 				unsigned char *lptr = str;
 				while (*lptr)
 				{
 					putNextChar(*lptr);
 					lptr++;
 				}
 			}
		}

		shiftHeld = shift;
		ctrlHeld  = ctrl;
		altHeld   = alt;
	}
	else
	{
		// currently libpspirkeyb seems bugged for keycodes on at least the
		// Targus.  It sends a huge stream of released presses, so we need to
		// filter for those keys that are actually pressed.
		if (gPressMap[keycode])
		{
#if DEBUG_OUTPUT >= 3
    	sioPrint("Keyrelease: ");
    	sioPrintWord(keycode);
    	sioPrint(" ");
    	sioPrintWord((shift << 2) | (ctrl << 1) | alt);
    	sioPrint("\r\n");
#endif

			gPressMap[keycode] = 0;
			numPressed --;

			shiftHeld = shift;
			ctrlHeld  = ctrl;
			altHeld   = alt;
		}
	}

}

wchar getNextCharW(void) {
  // block until queue is not empty
	int lempty = 1;
	SceUID lthid = sceKernelGetThreadId();

	while (lempty) {
		sceKernelWaitSema(queueSema, 1, 0);

		// pretend empty if there's an exclusive thread and we are not it
		if ((gExclusiveThread == 0) || (lthid == gExclusiveThread))
		{
			if (queueSize != 0)
			{
				lempty = 0;
			}
		}

		if (lempty)
		{
			sceKernelSignalSema(queueSema, 1);
			sceKernelDelayThread(5000);
		}
	}

	wchar lchar = inBuffer[queueOutputPos];
	queueOutputPos = (queueOutputPos + 1) % QUEUE_SIZE;
	queueSize --;

	sceKernelSignalSema(queueSema, 1);

	return(lchar);
}

char getNextChar(void) {
	// we call getNextCharW, then convert to 8bit
	wchar lchar = getNextCharW();

	// XXX: naive conversion func.
	return ((char) lchar);
}

int registerDisplayCallback(SceUID hookid)
{
	int lreturn = 0;

	sceKernelWaitSema(callbackSema, 1, 0);

	if (gNumDisplayCallbacks < MAX_DISPLAY_CALLBACKS)
	{
		gDisplayCallbacks[gNumDisplayCallbacks++] = hookid;
		lreturn = 1;

#if DEBUG_INFO>=2
		sioPrint("Registered display callback #");
		sioPrintHex(gNumDisplayCallbackss);
		sioPrint(" - ID ");
		sioPrintWord(hookid);
		sioPrint("\r\n");
#endif
	}

	sceKernelSignalSema(callbackSema, 1);

	return lreturn;
}

void drawKeyboardInfo(DRAWINFO *argument)
{
	int ii;

	sceKernelWaitSema(callbackSema, 1, 0);

	for (ii = 0; ii < gNumDisplayCallbacks; ii++)
	{
		sceKernelNotifyCallback(gDisplayCallbacks[ii], (int)argument);
	}

	sceKernelSignalSema(callbackSema, 1);
}

int isVshMode()
{
	return gvshmode;
}

int configOpen(const char *filename)
{
	int lreturn;

	sceKernelWaitSema(queueSema, 1, 0);

#if DEBUG_OUTPUT>=3
	sioPrint("configOpen: ");
	sioPrint(filename);
	sioPrint("\r\n");
#endif

	if (numConfigs == MAX_CONFIGS)
	{
		sceKernelSignalSema(queueSema, 1);
		return PIKEY_ERROR_TOO_MANY_CONFIGS;
	}

	int fh = sceIoOpen(filename, PSP_O_RDONLY, 0777);
	if (fh < 0)
	{
		sceKernelSignalSema(queueSema, 1);
		return fh;
	}

	int len = sceIoLseek32(fh, 0, SEEK_END);
	int rc = sceIoLseek32(fh, 0, SEEK_SET);
	if (rc < 0)
	{
		sioPrint("Error on file seek: ");
		sioPrintWord(rc);
		sioPrint("\r\n");
		sceKernelSignalSema(queueSema, 1);
		return rc;
	}

	// allocate mem to hold the file
	configData[numConfigs] = myMalloc(len + 1);
	if (configData[numConfigs] == NULL)
	{
		sceKernelSignalSema(queueSema, 1);
		return PIKEY_ERROR_NO_MEM;
	}


	// set progress pointer to the start of the file data
	configOffset[numConfigs] = configData[numConfigs];

#if DEBUG_OUTPUT>=3
	sioPrint("Read the file\r\n");
#endif

	// read the file into mem, null-terminate it.
	rc = sceIoRead(fh, configData[numConfigs], len);
	if (rc < len)
	{
		sceKernelSignalSema(queueSema, 1);
		return PIKEY_ERROR_MISC;
	}

	configData[numConfigs][len] = '\0';

#if DEBUG_OUTPUT>=3
	sioPrint("Close the file\r\n");
#endif

	sceIoClose(fh);

	lreturn = numConfigs++;

 	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

int configRead(int cfgHandle, char *token, char *value)
{
	int lreturn = 0;

	sceKernelWaitSema(queueSema, 1, 0);

	// valid handle?
	if ((cfgHandle >= numConfigs) ||
			(cfgHandle < 0) ||
			(configData[cfgHandle] == NULL) ||
		 	(configOffset[cfgHandle] == NULL))
	{
		sioPrint("configRead: Invalid config handle\r\n");
		lreturn = 0;
	}
	else
	{
		// We start from configOffset, and seek to an EOL or EOF
		int   ldone = 0;
		char *lendptr;
		char *lequalptr;

		while (!ldone)
		{
			lendptr = configOffset[cfgHandle];
			lequalptr = NULL;

			while ((*lendptr) && (*lendptr != '\n'))
			{
				if (*lendptr == '=')
				{
					lequalptr = lendptr;
				}
				lendptr++;
			}

			if ((configOffset[cfgHandle][0] == '#') &&
					(*lendptr != '\0'))
			{
				// this is a comment, and we're not at EOF.  Read the next line.
				configOffset[cfgHandle] = lendptr + 1;
			}
			else
			{
				ldone = 1;
			}
		}

		char *linestart = configOffset[cfgHandle];

		// found an equals sign that's not in the first column?
		if (lequalptr == NULL)
		{
			lreturn = 0;
		}
		else
		{
			// scan back through whitespace from the equals
			char *ltokenend = lequalptr - 1;

			while ((ltokenend > linestart) &&
						 ((*ltokenend == ' ') ||
							(*ltokenend == '\t')))
			{
				ltokenend--;
			}

			// token length too small or too big?
			int toklen = ltokenend - linestart + 1;
			if ((toklen == 0) || (toklen > 63))
			{
				lreturn = 0;
			}
			else
			{
				strncpy(token, linestart, toklen);
				token[toklen] = '\0';

				// ... and now the value.  Scan forward from the equals, through
				// any whitespace.
				char *lvalstart = lequalptr + 1;

				while ((lvalstart < lendptr) &&
							 ((*lvalstart == ' ') ||
								(*lvalstart == '\t')))
				{
					lvalstart++;
				}

				int lvallen = lendptr - lvalstart;

				// value too large?
				if (lvallen > 127)
				{
					lreturn = 0;
				}
				// we allow an empty value, but it is a special case (might be -ve length)
				else if (lvallen < 0)
				{
					value[0] = '\0';
					lreturn = 1;
				}
				else
				{
					strncpy(value, lvalstart, lvallen);
					value[lvallen] = '\0';

					// tweak for CRLF files - remove the CR if present.
					if ((lvallen >= 1) &&
							(value[lvallen-1] == '\r'))
					{
						value[lvallen-1] = '\0';
					}

					// all done, and we got a value of some sort.  SUCCESS!
					lreturn = 1;
				}
			}
		}

		// advance the config offset appropriately, ready for the next call.
		if (*lendptr == '\0')
		{
			// EOF
			configOffset[cfgHandle] = NULL;
		}
		else
		{
			// EOL
			configOffset[cfgHandle] = lendptr + 1;
		}
	}

 	sceKernelSignalSema(queueSema, 1);

	return lreturn;
}

void configClose(int cfgHandle)
{
	sceKernelWaitSema(queueSema, 1, 0);

	if ((cfgHandle >= numConfigs) ||
			(cfgHandle < 0) ||
			(configData[cfgHandle] == NULL))
	{
		sioPrint("Attempt to close bad config handle: ");
		sioPrintWord(cfgHandle);
		sioPrint("\r\n");
		sceKernelSignalSema(queueSema, 1);
		return;
	}

#if DEBUG_OUTPUT>=3
	sioPrint("Valid handle\r\n");
#endif

	// We don't do anything about freeing the config slot here,
	// since the cost of one slot is just 8 bytes, and we expect that MAX_CONFIGS
	// is plenty for all the config files we will be expected to process during
	// the lifetime of the PRX invocation.

	// free the mem
	myFree(configData[cfgHandle]);
	configData[cfgHandle] = NULL;
	configOffset[cfgHandle] = NULL;

 	sceKernelSignalSema(queueSema, 1);
}

int module_start(SceSize args, void *argp) __attribute__((alias("_start")));

int _start(SceSize args, void *argp)
{
	main();
	return 0;
}

/*****************************************************************************/
/* Shut everything down - kill the main thread, unload the modules.          */
/*****************************************************************************/
void terminateAll()
{
	terminate = 1;

	// this seems to fail to unload most of the time, or just crashes in
	// GAME150 mode - probably not worth running it, for the time being.
#if 0
	if (inputs != NULL)
	{
		INPUT_INFO *lptr = inputs;

		while (lptr)
		{
			int status, rc;

#if DEBUG_OUTPUT>=2
			sioPrint("Shutdown module (");
			sioPrintWord(lptr->moduleID);
			sioPrint(")...");
#endif

			rc = sceKernelStopModule(lptr->moduleID, 0, NULL, &status, NULL);
			if (rc < 0)
			{
#if DEBUG_OUTPUT>=2
				sioPrint("Failed to stop module: ");
				sioPrint(lptr->moduleName);
				sioPrint(": ");
				sioPrintWord(rc);
#endif
			}
			else
			{
				rc = sceKernelUnloadModule(lptr->moduleID);
				if (rc < 0)
				{
#if DEBUG_OUTPUT>=2
					sioPrint("Failed to unload module: ");
					sioPrint(lptr->moduleName);
					sioPrint(": ");
					sioPrintWord(rc);
#endif
				}
			}
			sioPrint("\r\n");

			lptr = lptr->next;
		}
	}

	if (outputs != NULL)
	{
		OUTPUT_INFO *lptr = outputs;

		while (lptr)
		{
			int status, rc;

#if DEBUG_OUTPUT>=2
			sioPrint("Shutdown module (");
			sioPrintWord(lptr->moduleID);
			sioPrint(")...");
#endif
			rc = sceKernelStopModule(lptr->moduleID, 0, NULL, &status, NULL);
			if (rc < 0)
			{
#if DEBUG_OUTPUT>=2
				sioPrint("Failed to stop module: ");
				sioPrint(lptr->moduleName);
				sioPrint(": ");
				sioPrintWord(rc);
#endif
			}
			else
			{
				rc = sceKernelUnloadModule(lptr->moduleID);
				if (rc < 0)
				{
#if DEBUG_OUTPUT>=2
					sioPrint("Failed to unload module: ");
					sioPrint(lptr->moduleName);
					sioPrint(": ");
					sioPrintWord(rc);
#endif
				}
			}
			sioPrint("\r\n");

			lptr = lptr->next;
		}
	}
#endif

	sceKernelWaitThreadEnd(pikeythread, 0);
}

/*****************************************************************************/
/* This is apparently never called when running as a VSH OE plugin.          */
/*****************************************************************************/
int module_stop(SceSize args, void *argp)
{
#if DEBUG_OUTPUT>=2
	sioPrint("---SHUTDOWN---\r\n");
#endif

  terminateAll();

	return 0;
}

int module_bootstart()
{
#if DEBUG_OUTPUT>=2
	sioPrint("---bootstart---\r\n");
#endif
	return 0;
}

/*****************************************************************************/
/* This gets called immediately before the transition from VSH to GAME mode  */
/* (and presumably vice-versa).                                              */
/*                                                                           */
/* We don't do anything with this call in piKey.                             */
/*****************************************************************************/
int module_reboot_before()
{
#if DEBUG_OUTPUT>=2
	sioPrint("---boot before---\r\n");
#endif

	return 0;
}

/*****************************************************************************/
/* Called 3 times during reboot to GAME mode. arg1 is 3, 2, 1 in the 3 			 */
/* calls, respectively.             																				 */
/*****************************************************************************/
int module_reboot_phase(int arg1)
{
#if DEBUG_OUTPUT>=2
	sioPrint("---boot phase--- ");
	sioPrintHex(arg1);
	sioPrint("\r\n");
#endif

	if (arg1 == 3)  // first reboot phase
	{
		terminateAll();
	}

	return 0;
}

#if 0
Lessons from this crash:
  - if a vsh kernel thread does not properly yield (must be delaythread,
		waitvblank does not count) then it will not be killed during the reboot
		to game mode.
  - you can detect the transition via the little-known module_reboot_* funcs
		and therefore end a thread loop that doesn't yield.
#endif
