/*****************************************************************************\
     Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

/***********************************************************************************
  SNES9X for Mac OS (c) Copyright John Stiles

  Snes9x for Mac OS X

  (c) Copyright 2001 - 2011  zones
  (c) Copyright 2002 - 2005  107
  (c) Copyright 2002         PB1400c
  (c) Copyright 2004         Alexander and Sander
  (c) Copyright 2004 - 2005  Steven Seeger
  (c) Copyright 2005         Ryan Vogt
 ***********************************************************************************/


#include "snes9x.h"
#include "memmap.h"
#include "apu.h"
#include "snapshot.h"
#include "cheats.h"
#include "display.h"

#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>

#include "mac-prefix.h"
#include "mac-cart.h"
#include "mac-cheatfinder.h"
#include "mac-controls.h"
#include "mac-dialog.h"
#include "mac-file.h"
#include "mac-joypad.h"
#include "mac-keyboard.h"
#include "mac-os.h"
#include "mac-snes9x.h"
#include "mac-stringtools.h"
#include "mac-netplay.h"
#include "mac-client.h"

#ifdef SELF_TEST
#include <sys/un.h>
#endif

#define KeyIsPressed(km, k)	(1 & (((unsigned char *) km) [(k) >> 3] >> ((k) & 7)))

enum
{
	kNPCDialogNone,
	kNPCDialogInit,
	kNPCDialogConnect,
	kNPCDialogConnectFailed,
	kNPCDialogOpenBegin,
	kNPCDialogOpenEnd,
	kNPCDialogPrepare,
	kNPCDialogPrepareFailed,
	kNPCDialogShowList,
	kNPCDialogDone,
	kNPCDialogCancel
};

typedef struct
{
	volatile bool8	padloop;
	volatile bool8	exitsgn;
	volatile uint32 phasecount;
	volatile uint32 phasespan;
	volatile uint8	header;
	bool8			online;
	int				socket;
	int				numplayers;
	char			name[256];
	char			serverIP[256];

	int				savedDeviceSetting;
	int				savedAutoSaveDelay;

	bool8			configsaved;
	bool8			dialogcancel;
	bool8			dialogsheet;
	int				dialogprocess;
}	clientState;

typedef struct
{
	bool8			ready;
	int				player;
	char			name[256];
}	clientsInfo;

typedef struct
{
	uint32			crc32;
	int				input;
	int				length;
	char			fname[PATH_MAX + 1];
}	cROMInfo;

static char			n_csememu[] = "/tmp/s9x_c_emu_semaphore",
					n_csempad[] = "/tmp/s9x_c_pad_semaphore";

static clientState	npclient;
static clientsInfo	npcinfo[NP_MAX_PLAYERS];

static cROMInfo		nprominfo;

static uint32		npcactvpad[NP_MAX_PLAYERS][64],	// [player number]
					npcrecvpad[NP_MAX_PLAYERS][64],	// [player number]
					npcsendpad[64],
					npccachpad[64];

static WindowRef	mRef, sRef;
static sem_t		*csememu, *csempad;
static pthread_t	connectthread, preparethread, gamepadthread;

static int NPClientGetMesFromServer (void);
static void NPClientDetachConnectThread (void);
static void NPClientDetachPrepareThread (void);
static void NPClientBeginPlayerListSheet (void);
static void NPClientEndPlayerListSheet (void);
static bool8 NPClientConnectToServer (int);
static bool8 NPClientSendMesToServer (int);
static bool8 NPClientSendNameToServer (void);
static bool8 NPClientGetROMInfoFromServer (void);
static bool8 NPClientBeginOpenROMImage (WindowRef);
static bool8 NPClientEndOpenROMImage (void);
static bool8 NPClientROMReadyToServer (void);
static bool8 NPClientGetSRAMFromServer (void);
static bool8 NPClientGetPlayerListFromServer (void);
static bool8 NPClientReplyPhaseSpanTest (void);
static void * NPClientConnectThread (void *);
static void * NPClientPrepareThread (void *);
static void * NPClientNetPlayThread (void *);
static pascal void NPClientDialogTimerHandler (EventLoopTimerRef, void *);
static pascal OSStatus NPClientDialogEventHandler (EventHandlerCallRef, EventRef, void *);
static pascal OSStatus NPClientSheetEventHandler (EventHandlerCallRef, EventRef, void *);


bool8 NPClientDialog (void)
{
	OSStatus	err;
	IBNibRef	nibRef;

	npclient.dialogcancel = true;
	npclient.dialogsheet  = false;
	npclient.configsaved  = false;

	err = CreateNibReference(kMacS9XCFString, &nibRef);
	if (err == noErr)
	{
		err = CreateWindowFromNib(nibRef, CFSTR("Connect"), &mRef);
		if (err == noErr)
		{
			err = CreateWindowFromNib(nibRef, CFSTR("PlayerList"), &sRef);
			if (err == noErr)
			{
				EventHandlerRef		eref, seref;
				EventLoopTimerRef	tref;
				EventHandlerUPP		eventUPP, sheetUPP;
				EventLoopTimerUPP	timerUPP;
				EventTypeSpec		windowEvents[] = { { kEventClassCommand, kEventCommandProcess      },
													   { kEventClassCommand, kEventCommandUpdateStatus } };
				CFStringRef			ref;
				HIViewRef			ctl, root;
				HIViewID			cid;

				npclient.dialogprocess = kNPCDialogInit;

				eventUPP = NewEventHandlerUPP(NPClientDialogEventHandler);
				err = InstallWindowEventHandler(mRef, eventUPP, GetEventTypeCount(windowEvents), windowEvents, (void *) mRef, &eref);

				timerUPP = NewEventLoopTimerUPP(NPClientDialogTimerHandler);
				err = InstallEventLoopTimer(GetCurrentEventLoop(), 0.0f, 0.1f, timerUPP, (void *) mRef, &tref);

				sheetUPP = NewEventHandlerUPP(NPClientSheetEventHandler);
				err = InstallWindowEventHandler(sRef, sheetUPP, GetEventTypeCount(windowEvents), windowEvents, (void *) sRef, &seref);

				root = HIViewGetRoot(mRef);
				cid.id = 0;

				cid.signature = 'CHAS';
				HIViewFindByID(root, cid, &ctl);
				HIViewSetVisible(ctl, false);

				cid.signature = 'SVIP';
				HIViewFindByID(root, cid, &ctl);
				SetEditTextCStr(ctl, npServerIP, false);

				cid.signature = 'CLNM';
				HIViewFindByID(root, cid, &ctl);
				ref = CFStringCreateWithCString(kCFAllocatorDefault, npName, kCFStringEncodingUTF8);
				if (ref)
				{
					SetEditTextCFString(ctl, ref, false);
					CFRelease(ref);
				}
				else
					SetEditTextCFString(ctl, CFSTR("unknown"), false);

				MoveWindowPosition(mRef, kWindowClient, false);
				ShowWindow(mRef);
				err = HIViewAdvanceFocus(root, 0);
				err = RunAppModalLoopForWindow(mRef);
				HideWindow(mRef);
				SaveWindowPosition(mRef, kWindowClient);

				err = RemoveEventHandler(seref);
				DisposeEventHandlerUPP(sheetUPP);

				err = RemoveEventLoopTimer(tref);
				DisposeEventLoopTimerUPP(timerUPP);

				err = RemoveEventHandler(eref);
				DisposeEventHandlerUPP(eventUPP);

				CFRelease(sRef);
			}

			CFRelease(mRef);
		}

		DisposeNibReference(nibRef);
	}

	return (!npclient.dialogcancel);
}

static pascal OSStatus NPClientDialogEventHandler (EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData)
{
	OSStatus	err, result = eventNotHandledErr;

	switch (GetEventClass(inEvent))
	{
		case kEventClassCommand:
			switch (GetEventKind(inEvent))
			{
				HICommand	tHICommand;

				case kEventCommandUpdateStatus:
					err = GetEventParameter(inEvent, kEventParamDirectObject, typeHICommand, NULL, sizeof(HICommand), NULL, &tHICommand);
					if (err == noErr && tHICommand.commandID == 'clos')
					{
						UpdateMenuCommandStatus(false);
						result = noErr;
					}

					break;

				case kEventCommandProcess:
					err = GetEventParameter(inEvent, kEventParamDirectObject, typeHICommand, NULL, sizeof(HICommand), NULL, &tHICommand);
					if (err == noErr)
					{
						switch (tHICommand.commandID)
						{
							case 'OK__':
								CFStringRef	ref;
								HIViewRef	ctl, root;
								HIViewID	cid;

								root = HIViewGetRoot(mRef);
								cid.id = 0;

								cid.signature = 'SVIP';
								HIViewFindByID(root, cid, &ctl);
								GetEditTextCStr(ctl, npclient.serverIP);
								DeactivateControl(ctl);
								if (npclient.serverIP[0] == 0)
									strcpy(npclient.serverIP, "127.0.0.1");
								strcpy(npServerIP, npclient.serverIP);
								printf("%s\n", npServerIP);

								cid.signature = 'CLNM';
								HIViewFindByID(root, cid, &ctl);
								CopyEditTextCFString(ctl, &ref);
								DeactivateControl(ctl);
								if (ref)
								{
									Boolean	r;

									r = CFStringGetCString(ref, npclient.name, 256, kCFStringEncodingUTF8);
									if (!r)
										strcpy(npclient.name, "unknown");
									else
									if (npclient.name[0] == 0)
										strcpy(npclient.name, "Guest");

									CFRelease(ref);
								}
								else
									strcpy(npclient.name, "unknown");
								strcpy(npName, npclient.name);
								printf("%s\n", npName);

								cid.signature = 'OK__';
								HIViewFindByID(root, cid, &ctl);
								DeactivateControl(ctl);

								cid.signature = 'NOT_';
								HIViewFindByID(root, cid, &ctl);
								DeactivateControl(ctl);

								npclient.dialogcancel  = false;
								npclient.dialogprocess = kNPCDialogConnect;

								result = noErr;
								break;

							case 'NOT_':
								npclient.dialogcancel  = true;
								npclient.dialogprocess = kNPCDialogCancel;

								result = noErr;
								break;

							case 'NvDn':
								npclient.dialogcancel  = false;
								npclient.dialogprocess = kNPCDialogOpenEnd;

								result = noErr;
								break;
						}
					}

					break;
			}

			break;
	}

	return (result);
}

static pascal void NPClientDialogTimerHandler (EventLoopTimerRef inTimer, void *userData)
{
	WindowRef	window = (WindowRef) userData;
	HIViewRef	ctl;
	HIViewID	cid = { 'CHAS', 0 };

	HIViewFindByID(HIViewGetRoot(mRef), cid, &ctl);

	switch (npclient.dialogprocess)
	{
		case kNPCDialogNone:
			break;

		case kNPCDialogCancel:
			NPNotification("  kNPCDialogCancel", -1);
			npclient.dialogprocess = kNPCDialogNone;
			npclient.dialogcancel  = true;
			QuitAppModalLoopForWindow(mRef);
			break;

		case kNPCDialogInit:
			NPNotification("  kNPCDialogInit", -1);
			npclient.dialogprocess = kNPCDialogNone;
			break;

		case kNPCDialogConnect:
			NPNotification("  kNPCDialogConnect", -1);
			npclient.dialogprocess = kNPCDialogNone;
			HIViewSetVisible(ctl, true);
			NPClientDetachConnectThread();
			break;

		case kNPCDialogConnectFailed:
			NPNotification("  kNPCDialogConnectFailed", -1);
			npclient.dialogprocess = kNPCDialogNone;
			npclient.dialogcancel  = true;
			QuitAppModalLoopForWindow(mRef);
			break;

		case kNPCDialogOpenBegin:
			NPNotification("  kNPCDialogOpenBegin", -1);
			npclient.dialogprocess = kNPCDialogNone;
			HIViewSetVisible(ctl, false);
			NPClientStoreConfig();
			if (!NPClientBeginOpenROMImage(window))
			{
				NPClientDisconnect();
				NPClientRestoreConfig();
				npclient.dialogprocess = kNPCDialogCancel;
			}

			break;

		case kNPCDialogOpenEnd:
			NPNotification("  kNPCDialogOpenEnd", -1);
			npclient.dialogprocess = kNPCDialogNone;
			if (!NPClientEndOpenROMImage())
			{
				NPClientDisconnect();
				NPClientRestoreConfig();
				npclient.dialogprocess = kNPCDialogCancel;
			}
			else
				npclient.dialogprocess = kNPCDialogPrepare;

			break;

		case kNPCDialogPrepare:
			NPNotification("  kNPCDialogPrepare", -1);
			npclient.dialogprocess = kNPCDialogNone;
			HIViewSetVisible(ctl, true);
			NPClientDetachPrepareThread();
			break;

		case kNPCDialogPrepareFailed:
			NPNotification("  kNPCDialogPrepareFailed", -1);
			npclient.dialogprocess = kNPCDialogNone;
			NPClientRestoreConfig();
			npclient.dialogcancel = true;
			QuitAppModalLoopForWindow(mRef);
			break;

		case kNPCDialogShowList:
			NPNotification("  kNPCDialogShowList", -1);
			npclient.dialogprocess = kNPCDialogNone;
			HIViewSetVisible(ctl, false);
			npclient.dialogsheet = true;
			NPClientBeginPlayerListSheet();
			break;

		case kNPCDialogDone:
			NPNotification("  kNPCDialogDone", -1);
			npclient.dialogprocess = kNPCDialogNone;
			NPClientEndPlayerListSheet();
			npclient.dialogsheet  = false;
			npclient.dialogcancel = false;
			QuitAppModalLoopForWindow(mRef);
			break;
	}
}

static void NPClientDetachConnectThread (void)
{
	pthread_create(&connectthread, NULL, NPClientConnectThread, NULL);
	pthread_detach(connectthread);
}

static void * NPClientConnectThread (void *)
{
	NPNotification("Client: Entered connection thread.", -1);

	if ((NPClientConnectToServer(NP_PORT) == false) ||
		(NPClientSendNameToServer()       == false) ||
		(NPClientGetROMInfoFromServer()   == false))
	{
		NPClientDisconnect();
		npclient.dialogprocess = kNPCDialogConnectFailed;
		return (NULL);
	}

	npclient.dialogprocess = kNPCDialogOpenBegin;
	NPNotification("Client: Exited connection thread.", -1);
	return (NULL);
}

void NPClientInit (void)
{
	npclient.padloop     = false;
	npclient.exitsgn     = false;
	npclient.phasecount  = 0;
	npclient.phasespan   = 0;
	npclient.header      = 0;
	npclient.online      = false;
	npclient.socket      = -1;
	npclient.numplayers  = 0;
	npclient.name[0]     = 0;
	npclient.serverIP[0] = 0;

	nprominfo.crc32      = 0;
	nprominfo.input      = 0;
	nprominfo.length     = 0;
	nprominfo.fname[0]   = 0;

	for (int i = 0; i < NP_MAX_PLAYERS; i++)
	{
		for (int j = 0; j < 64; j++)
		{
			npcactvpad[i][j] = 0;
			npcrecvpad[i][j] = 0;
		}
	}

	for (int j = 0; j < 64; j++)
	{
		npcsendpad[j] = 0;
		npccachpad[j] = 0;
	}

	for (int c = 0; c < NP_MAX_PLAYERS; c++)
	{
		npcinfo[c].ready   = false;
		npcinfo[c].player  = 0;
		npcinfo[c].name[0] = 0;
	}
}

static bool8 NPClientConnectToServer (int port)
{
#ifndef SELF_TEST
	struct sockaddr_in	address;
#else
	struct sockaddr_un	address;
#endif

	NPNotification("Client: Connecting to server...", -1);

	memset(&address, 0, sizeof(address));
#ifndef SELF_TEST
	address.sin_family      = AF_INET;
	address.sin_addr.s_addr = inet_addr(npclient.serverIP);
	address.sin_port        = htons(port);
#else
	address.sun_family      = AF_UNIX;
	strcpy(address.sun_path, SOCK_NAME);
#endif

#ifndef SELF_TEST
	if (address.sin_addr.s_addr == INADDR_NONE)
	{
		NPError("Client: Server IP is invalid.", 5001);
		return (false);
	}
#endif

#ifndef SELF_TEST
	if ((npclient.socket = socket(PF_INET, SOCK_STREAM, 0)) < 0)
#else
	if ((npclient.socket = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
#endif
	{
		NPError("Client: Failed to create socket.", 5002);
		return (false);
	}

	if (connect(npclient.socket, (struct sockaddr *) &address, sizeof(address)) < 0)
	{
		NPError("Client: Failed to connect to server.", 5003);
		return (false);
	}

	npclient.online = true;

	NPNotification("Client: Connected to server.", -1);
	return (true);
}

void NPClientDisconnect (void)
{
	if (npclient.socket != -1)
	{
		NPNotification("Client: Disconnecting from server...", -1);

		close(npclient.socket);
		npclient.socket = -1;

		NPNotification("Client: Disconnected from server.", -1);
	}

	npclient.online      = false;
	npclient.name[0]     = 0;
	npclient.serverIP[0] = 0;
}

static bool8 NPClientSendMesToServer (int num)
{
	uint8	mes[2];

	mes[0] = NP_CLIENT_MAGIC;
	mes[1] = num;

	if (socket_write(npclient.socket, mes, 2) != 2)
		return (false);

	return (true);
}

static int NPClientGetMesFromServer (void)
{
	uint8	mes[2];

	if (socket_read(npclient.socket, mes, 2) != 2)
		return (-1);

	if (mes[0] != NP_SERVER_MAGIC)
		return (-1);

	return ((int) mes[1]);
}

static bool8 NPClientSendNameToServer (void)
{
	if (!npclient.online)
		return (false);

	NPNotification("Client: Sending player name to server...", -1);

	if (NPClientGetMesFromServer() != kNPServerNameRequest)
	{
		NPError("Client: Failed to receive messsage from server.", 5101);
		return (false);
	}

	uint8	mes[4];
	uint32	l;

	l = strlen(npclient.name);
	WRITE_LONG(mes + 0, l);

	if (socket_write(npclient.socket, mes, 4) != 4)
	{
		NPError("Client: Failed to send name size to server.", 5102);
		return (false);
	}

	if (socket_write(npclient.socket, (uint8 *) npclient.name, l) != (int) l)
	{
		NPError("Client: Failed to send name to server.", 5103);
		return (false);
	}

	if (NPClientGetMesFromServer() != kNPServerNameReceived)
	{
		NPError("Client: Failed to receive messsage from server.", 5104);
		return (false);
	}

	if (NPClientSendMesToServer(kNPClientNameSent) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5105);
		return (false);
	}

	NPNotification("Client: Sent player name to server.", -1);
	return (true);
}

static bool8 NPClientGetROMInfoFromServer (void)
{
	if (!npclient.online)
		return (false);

	NPNotification("Client: Receiving ROM information from server...", -1);

	if (NPClientGetMesFromServer() != kNPServerROMInfoWillSend)
	{
		NPError("Client: Failed to receive messsage from server.", 5201);
		return (false);
	}

	if (NPClientSendMesToServer(kNPClientROMInfoWaiting) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5202);
		return (false);
	}

	uint8	mes[16];
	uint32	l;

	if (socket_read(npclient.socket, mes, 16) != 16)
	{
		NPError("Client: Failed to receive ROM information from server.", 5203);
		return (false);
	}

	nprominfo.crc32      = READ_LONG(mes + 0);
	nprominfo.input      = READ_LONG(mes + 4);

	l = READ_LONG(mes + 12);

	if (socket_read(npclient.socket, (uint8 *) nprominfo.fname, l) != (int) l)
	{
		NPError("Client: Failed to receive ROM name from server.", 5204);
		return (false);
	}

	nprominfo.fname[l] = 0;
	nprominfo.length   = l;

	NPNotification("Client: Received ROM information from server.", -1);
	return (true);
}

void NPClientStoreConfig (void)
{
	npclient.savedDeviceSetting = deviceSetting;
	npclient.savedAutoSaveDelay = Settings.AutoSaveDelay;

	npclient.configsaved = true;

	deviceSetting = nprominfo.input;
	Settings.AutoSaveDelay = 0;

	ChangeInputDevice();
}

void NPClientRestoreConfig (void)
{
	if (npclient.configsaved)
	{
		deviceSetting = npclient.savedDeviceSetting;
		Settings.AutoSaveDelay = npclient.savedAutoSaveDelay;

		npclient.configsaved = false;

		ChangeInputDevice();
	}
}

static bool8 NPClientBeginOpenROMImage (WindowRef window)
{
	CFStringRef			numRef, romRef, baseRef;
	CFMutableStringRef	mesRef;
	SInt32				replaceAt;
	bool8				r;

	DeinitGameWindow();

	if (cartOpen)
	{
		SNES9X_SaveSRAM();
		S9xResetSaveTimer(false);
		S9xSaveCheatFile(S9xGetFilename(".cht", CHEAT_DIR));
	}

	cartOpen = false;

	ResetCheatFinder();

	romRef  = CFStringCreateWithCString(kCFAllocatorDefault, nprominfo.fname, kCFStringEncodingUTF8);
	numRef  = CFCopyLocalizedString(CFSTR("NPROMNamePos"), "1");
	baseRef = CFCopyLocalizedString(CFSTR("NPROMNameMes"), "NPROM");
	mesRef  = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, baseRef);
	replaceAt = CFStringGetIntValue(numRef);
	CFStringReplace(mesRef, CFRangeMake(replaceAt - 1, 1), romRef);

	r = NavBeginOpenROMImageSheet(window, mesRef);

	CFRelease(mesRef);
	CFRelease(baseRef);
	CFRelease(numRef);
	CFRelease(romRef);

	return (r);
}

static bool8 NPClientEndOpenROMImage (void)
{
	OSStatus	err;
	FSRef		cartRef;
	char		filename[PATH_MAX + 1];
	bool8		r;

	r = NavEndOpenROMImageSheet(&cartRef);
	if (!r)
	{
		cartOpen = false;
		return (false);
	}

	CheckSaveFolder(&cartRef);

	Settings.ForceLoROM          = (romDetect        == kLoROMForce       );
	Settings.ForceHiROM          = (romDetect        == kHiROMForce       );
	Settings.ForceHeader         = (headerDetect     == kHeaderForce      );
	Settings.ForceNoHeader       = (headerDetect     == kNoHeaderForce    );
	Settings.ForceInterleaved    = (interleaveDetect == kInterleaveForce  );
	Settings.ForceInterleaved2   = (interleaveDetect == kInterleave2Force );
	Settings.ForceInterleaveGD24 = (interleaveDetect == kInterleaveGD24   );
	Settings.ForceNotInterleaved = (interleaveDetect == kNoInterleaveForce);
	Settings.ForcePAL            = (videoDetect      == kPALForce         );
	Settings.ForceNTSC           = (videoDetect      == kNTSCForce        );

	GFX.InfoString = NULL;
	GFX.InfoStringTimeout = 0;

	S9xResetSaveTimer(true);

	err = FSRefMakePath(&cartRef, (unsigned char *) filename, PATH_MAX);

	SNES9X_InitSound();

	if (Memory.LoadROM(filename) /*&& (Memory.ROMCRC32 == nprominfo.crc32)*/)
	{
		ChangeTypeAndCreator(filename, 'CART', '~9X~');
		cartOpen = true;
		return (true);
	}
	else
	{
		cartOpen = false;
		return (false);
	}
}

static void NPClientDetachPrepareThread (void)
{
	pthread_create(&preparethread, NULL, NPClientPrepareThread, NULL);
	pthread_detach(preparethread);
}

static void * NPClientPrepareThread (void *)
{
	NPNotification("Client: Entered preparing thread.", -1);

	if ((NPClientROMReadyToServer()        == false) ||
		(NPClientGetSRAMFromServer()       == false) ||
		(NPClientGetPlayerListFromServer() == false) ||
		(NPClientReplyPhaseSpanTest()      == false))
	{
		NPClientDisconnect();
		npclient.dialogprocess = kNPCDialogPrepareFailed;
		return (NULL);
	}

	npclient.dialogprocess = kNPCDialogShowList;
	NPNotification("Client: Exited preparing thread.", -1);
	return (NULL);
}

static bool8 NPClientROMReadyToServer (void)
{
	if (!npclient.online)
		return (false);

	NPNotification("Client: Sending ROM ready sign to server...", -1);

	if (NPClientSendMesToServer(kNPClientROMOpened) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5401);
		return (false);
	}

	NPNotification("Client: Sent ROM ready sign to server.", -1);
	return (true);
}

static bool8 NPClientGetSRAMFromServer (void)
{
	if (!npclient.online)
		return (false);

	NPNotification("Client: Receiving SRAM from server...", -1);

	if (NPClientGetMesFromServer() != kNPServerSRAMWillSend)
	{
		NPError("Client: Failed to receive messsage from server.", 5501);
		return (false);
	}

	if (NPClientSendMesToServer(kNPClientSRAMWaiting) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5502);
		return (false);
	}

	uint8	mes[4];
	uint32	sramsize;

	if (socket_read(npclient.socket, mes, 4) != 4)
	{
		NPError("Client: Failed to receive SRAM size from server.", 5503);
		return (false);
	}

	sramsize = READ_LONG(mes + 0);

	if (sramsize != (uint32) (Memory.SRAMSize ? (1 << (Memory.SRAMSize + 3)) * 128 : 0))
	{
		NPError("Client: SRAM size mismatch.", 5504);
		return (false);
	}

	if (sramsize && (socket_read(npclient.socket, Memory.SRAM, sramsize) != (int) sramsize))
	{
		NPError("Server: Failed to receive SRAM from server.", 5505);
		return (false);
	}

	if (NPClientSendMesToServer(kNPClientSRAMLoaded) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5506);
		return (false);
	}

	NPNotification("Client: Received SRAM from server.", -1);
	return (true);
}

static bool8 NPClientGetPlayerListFromServer (void)
{
	if (!npclient.online)
		return (false);

	NPNotification("Client: Receiving player list from server...", -1);

	if (NPClientGetMesFromServer() != kNPServerPlayerWillSend)
	{
		NPError("Client: Failed to receive messsage from server.", 5701);
		return (false);
	}

	if (NPClientSendMesToServer(kNPClientPlayerWaiting) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5702);
		return (false);
	}

	for (int i = 0; i < NP_MAX_PLAYERS; i++)
	{
		uint8	mes[10];
		uint32	l;

		if (socket_read(npclient.socket, mes, 10) != 10)
		{
			NPError("Client: Failed to receive messsage from server.", 5703);
			return (false);
		}

		npcinfo[i].ready  = READ_BYTE(mes + 1);
		npcinfo[i].player = READ_LONG(mes + 2);

		l = READ_LONG(mes + 6);

		if (l && (socket_read(npclient.socket, (uint8 *) npcinfo[i].name, l) != (int) l))
		{
			NPError("Client: Failed to receive messsage from server.", 5704);
			return (false);
		}

		npcinfo[i].name[l] = 0;
	}

	npclient.numplayers = 0;
	for (int i = 0; i < NP_MAX_PLAYERS; i++)
		if (npcinfo[i].ready)
			npclient.numplayers++;

	NPNotification("Client: Received player list from server.", -1);
	NPNotification("Client: Number of players: %d", npclient.numplayers);

	return (true);
}

static bool8 NPClientReplyPhaseSpanTest (void)
{
	uint8   mes[21];
	int		l = npclient.numplayers * 4 + 1;

	NPNotification("Client: Replying sending / receiving pad states test...", -1);

	for (int n = 0; n < 5; n++)
	{
		if (socket_read(npclient.socket, mes, l) != l)
			return (false);

		if (socket_write(npclient.socket, mes, 5) != 5)
			return (false);
	}

	NPNotification("Client: Replied sending / receiving pad states test.", -1);

	NPNotification("Client: Receiving phase span value from server...", -1);

	if (socket_read(npclient.socket, mes, 4) != 4)
		return (false);

	npclient.phasespan = READ_LONG(mes + 0);

	NPNotification("  phase span: %d (frames)", npclient.phasespan);

	NPNotification("Client: Received phase span value from server.", -1);

	return (true);
}

static void NPClientBeginPlayerListSheet (void)
{
	OSStatus	err;
	CFStringRef	ref;
	HIViewRef	ctl, root;
	HIViewID	cid;

	root = HIViewGetRoot(sRef);
	cid.signature = 'PLNM';

	for (int i = 0; i < NP_MAX_PLAYERS; i++)
	{
		if (npcinfo[i].ready)
		{
			cid.id = npcinfo[i].player;
			HIViewFindByID(root, cid, &ctl);
			ref = CFStringCreateWithCString(kCFAllocatorDefault, npcinfo[i].name, kCFStringEncodingUTF8);
			if (ref)
			{
				SetStaticTextCFString(ctl, ref, false);
				CFRelease(ref);
			}
			else
				SetStaticTextCFString(ctl, CFSTR("unknown"), false);
		}
	}

	err = ShowSheetWindow(sRef, mRef);
}

static void NPClientEndPlayerListSheet (void)
{
	OSStatus	err;

	err = HideSheetWindow(sRef);
}

static pascal OSStatus NPClientSheetEventHandler (EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData)
{
	if (!npclient.dialogsheet)
		return (eventNotHandledErr);

	OSStatus	err, result = eventNotHandledErr;

	switch (GetEventClass(inEvent))
	{
		case kEventClassCommand:
			switch (GetEventKind(inEvent))
			{
				HICommand	tHICommand;

				case kEventCommandUpdateStatus:
					err = GetEventParameter(inEvent, kEventParamDirectObject, typeHICommand, NULL, sizeof(HICommand), NULL, &tHICommand);
					if (err == noErr && tHICommand.commandID == 'clos')
					{
						UpdateMenuCommandStatus(false);
						result = noErr;
					}

					break;

				case kEventCommandProcess:
					err = GetEventParameter(inEvent, kEventParamDirectObject, typeHICommand, NULL, sizeof(HICommand), NULL, &tHICommand);
					if (err == noErr)
					{
						switch (tHICommand.commandID)
						{
							case 'ok  ':
								npclient.dialogprocess = kNPCDialogDone;
								result = noErr;
								break;
						}
					}

					break;
			}

			break;
	}

	return (result);
}

void NPClientDetachNetPlayThread (void)
{
	NPNotification("Client: Detaching pad thread...", -1);

	npclient.padloop = true;
	npclient.exitsgn = false;

	csememu = sem_open(n_csememu, O_CREAT, 0600, 0);
	csempad = sem_open(n_csempad, O_CREAT, 0600, 0);

	pthread_create(&gamepadthread, NULL, NPClientNetPlayThread, NULL);
	pthread_detach(gamepadthread);

	NPNotification("Client: Detached pad thread.", -1);
}

void NPClientStopNetPlayThread (void)
{
	NPNotification("Client: Stopping pad thread...", -1);

	npclient.padloop = false;
	sem_post(csempad);
	sem_post(csememu);

	while (!npclient.exitsgn)
		sleep(0);

	sem_unlink(n_csememu);
	sem_unlink(n_csempad);
	sem_close(csememu);
	sem_close(csempad);

	NPNotification("Client: Stopped pad thread.", -1);
}

bool8 NPClientNetPlayWaitStart (void)
{
	NPNotification("Client: Waiting start flag...", -1);

	if (NPClientSendMesToServer(kNPClientStartWait) == false)
	{
		NPError("Client: Failed to send messsage to server.", 5801);
		return (false);
	}

	if (NPClientGetMesFromServer() != kNPServerStart)
	{
		NPError("Client: Failed to send messsage to server.", 5802);
		return (false);
	}

	npclient.phasecount = 0;
	npclient.header     = 0;

	sem_post(csempad);

	NPNotification("Client: Netplay started.", -1);
	return (true);
}

static void * NPClientNetPlayThread (void *)
{
	uint8	mes[NP_MAX_PLAYERS * 64 * 4 + 1];
	uint8	count = 0;
	int		l;

	NPNotification("Client: Entered pad thread.", -1);

	while (npclient.padloop)
	{
		sem_wait(csempad);

		l = npclient.numplayers * npclient.phasespan * 4 + 1;
		if (socket_read(npclient.socket, mes, l) != l)
		{
			npclient.exitsgn = true;
			sem_post(csememu);
			pthread_exit(NULL);
		}

		if ((mes[0] & 0xF) != count)
			NPNotification("Client: Warning: Failed to synchronize server.", -1);

		npclient.header = mes[0] & 0xF0;

		for (int i = 0; i < npclient.numplayers; i++)
			for (uint32 j = 0; j < npclient.phasespan; j++)
				npcrecvpad[i][j] = READ_LONG(mes + (i * npclient.phasespan + j) * 4 + 1);

		WRITE_BYTE(mes + 0, count);

		for (uint32 j = 0; j < npclient.phasespan; j++)
			WRITE_LONG(mes + j * 4 + 1, npcsendpad[j]);

		l = npclient.phasespan * 4 + 1;
		if (socket_write(npclient.socket, mes, l) != l)
		{
			npclient.exitsgn = true;
			sem_post(csememu);
			pthread_exit(NULL);
		}

		count = (count + 1) & 0xF;

		sem_post(csememu);
	}

	npclient.exitsgn = true;

	NPNotification("Client: Exited pad thread.", -1);
	return (NULL);
}

void NPClientProcessInput (void)
{
	static uint32	pos = 0;
	KeyMap			myKeys;

	if (npclient.exitsgn)
	{
		if (s9xthreadrunning)
		{
			if (!eventQueued)
			{
				PostQueueToSubEventLoop();
				eventQueued = true;
			}
		}
		else
			running = false;

		return;
	}

	if (npclient.phasecount == 0)
	{
		sem_wait(csememu);

		for (int i = 0; i < npclient.numplayers; i++)
			for (uint32 j = 0; j < npclient.phasespan; j++)
				npcactvpad[i][j] = npcrecvpad[i][j];

		for (uint32 j = 0; j < npclient.phasespan; j++)
			npcsendpad[j] = npccachpad[j];

		if (npclient.header & 0x80)
		{
			npcsendpad[npclient.phasespan] = 0;
			for (int i = 0; i < npclient.numplayers; i++)
				npcactvpad[i][npclient.phasespan] = 0;

			npclient.phasespan++;
			if (npclient.phasespan > (uint32) Memory.ROMFramesPerSecond)
				npclient.phasespan = (uint32) Memory.ROMFramesPerSecond;

			char	str[256];
			sprintf(str, "delay: %d", npclient.phasespan);
			S9xMessage(0, 0, str);
		}
		else
		if (npclient.header & 0x40)
		{
			npclient.phasespan--;
			if (npclient.phasespan == 0)
				npclient.phasespan = 1;

			char	str[256];
			sprintf(str, "delay: %d", npclient.phasespan);
			S9xMessage(0, 0, str);
		}

		npclient.header = 0;
		pos = 0;
	}

	for (int i = 0; i < npclient.numplayers; i++)
	{
 		controlPad[i] = npcactvpad[i][pos];
		ControlPadFlagsToS9xReportButtons(i, controlPad[i]);
	}

	GetKeys(myKeys);

	uint32  pad = 0;

	JoypadScanDirection(0, &pad);
	if (ISpKeyIsPressed(kISp1PR     ))	pad |= 0x0010;
	if (ISpKeyIsPressed(kISp1PL     ))	pad |= 0x0020;
	if (ISpKeyIsPressed(kISp1PX     ))	pad |= 0x0040;
	if (ISpKeyIsPressed(kISp1PA     ))	pad |= 0x0080;
	if (ISpKeyIsPressed(kISp1PStart ))	pad |= 0x1000;
	if (ISpKeyIsPressed(kISp1PSelect))	pad |= 0x2000;
	if (ISpKeyIsPressed(kISp1PY     ))	pad |= 0x4000;
	if (ISpKeyIsPressed(kISp1PB     ))	pad |= 0x8000;

	if (KeyIsPressed(myKeys, keyCode[k1PR]     ))	pad |= 0x0010;
	if (KeyIsPressed(myKeys, keyCode[k1PL]     ))	pad |= 0x0020;
	if (KeyIsPressed(myKeys, keyCode[k1PX]     ))	pad |= 0x0040;
	if (KeyIsPressed(myKeys, keyCode[k1PA]     ))	pad |= 0x0080;
	if (KeyIsPressed(myKeys, keyCode[k1PRight] ))	pad |= 0x0100;
	if (KeyIsPressed(myKeys, keyCode[k1PLeft]  ))	pad |= 0x0200;
	if (KeyIsPressed(myKeys, keyCode[k1PDown]  ))	pad |= 0x0400;
	if (KeyIsPressed(myKeys, keyCode[k1PUp]    ))	pad |= 0x0800;
	if (KeyIsPressed(myKeys, keyCode[k1PStart] ))	pad |= 0x1000;
	if (KeyIsPressed(myKeys, keyCode[k1PSelect]))	pad |= 0x2000;
	if (KeyIsPressed(myKeys, keyCode[k1PY]     ))	pad |= 0x4000;
	if (KeyIsPressed(myKeys, keyCode[k1PB]     ))	pad |= 0x8000;

	npccachpad[pos] = pad;

	if (npclient.phasecount == 0)
	{
		npclient.phasecount = npclient.phasespan;
		sem_post(csempad);
	}

	npclient.phasecount--;
	pos++;
}
