// HighScoreServer.m
// under development; anything with ***** is yet to be completed

// This class is the actual high score server for any given game.
// You shouldn't need to subclass it:  Just change the class of
// HighScoreSlot used and change the GameInfo object's parameters.

// I need to do exception handling in here to catch any DO problems,
// but haven't got around to it yet, so exceptions will currently crash
// the server. :-<  *****

// Note that a game with multiple tables simply uses multiple servers,
// each with a different name.  (For example:  NX_Invaders.easy,
// NX_Invaders.hard would be a possibility.  The HighScoreController in
// the game itself handles all the coordination between tables; by
// default they are numbered.)

// In the future it would be nice to allow the game to send a .o file
// over the connection for any high score slots the server can't deal
// with.  This would allow the server to truly dynamically update itself!
// Right now, though, there's not an easy way to do this and I don't have
// the time to take care of it, since other parts of the GameKit need more
// attention.  The method frameworks are in the protocol, though.

#import <appkit/appkit.h>
#import <daymisckit/daymisckit.h>
#import <gamekit/gamekit.h>
#import <remote/NXProxy.h>
#import <objc/objc-runtime.h>
#import <objc/objc-load.h>

// this is where the files are stored.  Each file is named
// using the convention "<game's name>.highscores"  The files
// themselves are all typed streams with archived GameInfo and
// HighScoreTable objects in them.  Override the -pathToTables
// method to change this.
#define PATH_TO_TABLES "/usr/local/games/highscores/"

static BOOL loggingIsOn;	// a flag to turn all logging on or
	// off for all servers in an application
static id logFile;
//  define DEBUGLOG for even more logging...

@implementation HighScoreServer

+ turnLoggingOn:(BOOL)flag
{
	loggingIsOn = flag;
	if (loggingIsOn) { // make new log file when turning on logging
		// (This allows us to change the name of the file by toggling
		// the on and off.  A subclass would have to implement +makeLogFile
		// in order to actually do something like this.)
		if (logFile) [logFile free];
		logFile = [self makeLogFile];
	}
	return self;
}

+ makeLogFile	// override to change where loggin goes to.
{				// Just return a DAYLogFile instance!
	id fileName = [[DAYString newWithString:PATH_TO_TABLES] cat:"log"];
	// I don't use an attendant lock file since there should only
	// ever be one server after the file -- note that I'm intending
	// this facility for remote servers, not local.  If you are going
	// to do logging from a local game, you _better_ give it a lock file!!!
	id newLog = [[[DAYLogFile alloc] init] setFileName:fileName];
	return newLog;
}

- init	{ return [self error:"Use -initForGame: (not -init)"]; }
- initForGame:(const char *)name // designated initializer
{
	[super init];
	if (!name) { // must give us a name or we'll barf.
#ifdef DEBUGLOG
		if (loggingIsOn) {
			id tempString = [DAYString
				newWithString:"An attempt was made to start a NULL server."];
			[logFile addLineToLogFile:tempString];
			[tempString free];
		}
#endif
		fprintf(stderr, "HighScoreServer error:  can't init for null game.\n");
		[self free];  
		return nil; // note that nothing is returned...
	}
	// find file that holds the high score file
	// if non-existent, create an empty table, otherwise we load it in
	gameName = [[DAYString alloc] initString:name];
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Starting server.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
	clientList = [[List alloc] initCount:0];
	connList = [[List alloc] initCount:0];
	clientAuth = [[Storage alloc]
			initCount:0 elementSize:sizeof(char) description:"c"];
	[[[GameInfo alloc] init] free];	// forces class to be linked into serverd
	// without the need for the ld flag that links in the whole library...
	gameInfo = nil;	// we'll force the first client to check in to send it	
	// build the name of the file where we store the highscores
	scoreFile = [[DAYString alloc] initString:[self pathToTables]];
	[scoreFile cat:"/"];
	[scoreFile concatenate:gameName];
	[scoreFile cat:".highscores"];
	[self load];	// load in a highscore table
	return self;
}

- (const char *)pathToTables
{	// subclass can override this to customize.
	return PATH_TO_TABLES;
}

- (oneway)addSlotCode:(bycopy in id)code	// not yet implemented
{ // *****
	if (loggingIsOn) {
		id tempString = [[gameName copy]
			cat:":  Slot code sent to server.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
	return self;
}

- (oneway)setGameInfo:(bycopy in id)info
{
	id emptySlotClass;
	const char *slotClass = [[info slotType] stringValue];
	
	// avoid freeing GameInfo if in a game and not a remote server
	if (![NXApp delegate]) if (gameInfo) [gameInfo free];
	gameInfo = info;	// we now have a gameInfo object that tells us
	// how to do things.  Without this, we can only function according
	// to the gamekit defaults.
	emptySlotClass = objc_lookUpClass(slotClass);
	if (!emptySlotClass) {	// can't find the class, so try and load it
		long ret;
		char *fileName = (char *)malloc(strlen(slotClass) +
				strlen([self pathToTables]) + 3);
		char *fileNames[2] = {fileName, NULL};
		sprintf(fileName, "%s%s.o", [self pathToTables], slotClass);
		ret = objc_loadModules(fileNames, NULL, NULL, NULL, NULL);
		emptySlotClass = objc_lookUpClass(slotClass);
		if (ret || !emptySlotClass) {
			// couldn't load the class so tell the clients we can't help.
			[clientList makeObjectsPerform:@selector(cantBeServed:)
					with:gameName];
			if (![NXApp delegate]) [gameInfo free];
			gameInfo = nil; // assume we're still uninitted.
			return self;
		}
	}
	[table setEmptySlotClass:emptySlotClass];
	[table setMaxHighScores:[gameInfo maxHighScores]];
	// ***** need to figure out which table we are so that this works right
	[table setMaxScoresPerPlayer:[gameInfo maxScoresPerPlayerTable:0 net:YES]];
#ifdef NOISYDEBUG
	fprintf(stderr, "HighScoreServer:  maxScorePerPlayer is %d.\n",
			[gameInfo maxScoresPerPlayerTable:0 net:YES]);
#endif
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Got GameInfo object.\n"];
		[logFile addLineToLogFile:tempString];
		[gameInfo dumpToLog:logFile];
		[tempString free];
	}
	[self save]; // so even if no slot is sent we at least have GameInfo saved.
	return self;
}

- (oneway)setTemplate:(bycopy in id)newTemplate
{
	if (template) [template free];
	template = newTemplate;
	if (haveNonTemplateTable) {
		int i;
		haveNonTemplateTable = NO;
		[self _makeTableRatherThanLoad];
		for (i=0; i<[clientList count]; i++)
			[[clientList objectAt:i] acceptTable:table name:gameName];
	}
	if (loggingIsOn) {
		id tempString = [[gameName copy]
				cat:":  Got table template object.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
	return self;
}

- free
{
	// free our private strings
	[scoreFile free];
	[gameName free];
	// free all high score slots and tables
	[[table freeObjects] free];
	// free other items (internal params)
	[clientList free];
	[clientAuth free];
	[connList free];
	if (![NXApp delegate]) [gameInfo free];
	return [super free];
}

// methods that the client can call
- (oneway)clientDying:(in id <HighScoreClient>)client
		// alerts server that a client is going away
{
	// remove client from the list
	unsigned num = [clientList indexOf:client];
	while (num != NX_NOT_IN_LIST) {
		[clientList removeObject:client];
		[connList removeObjectAt:num];
		[clientAuth removeElementAt:num];
		num = [clientList indexOf:client];
	}
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  A client left.\n"];
		[logFile addLineToLogFile:tempString];
		[gameInfo dumpToLog:logFile];
		[tempString free];
	}
#endif
	return self;
}

- senderIsInvalid:sender
{
	int i;
	BOOL changed = YES;
	while (changed) {
		changed = NO;
		for (i=0; i<[connList count]; i++) {
			if (sender == [connList objectAt:i]) {
				[clientList removeObjectAt:i];
				[connList removeObjectAt:i];
				[clientAuth removeElementAt:i];
				changed = YES;
				break; // for loop -- have to restart iteration, since
				// list object has been changed now, but we also
				// want to be sure that we remove multiple pointers
				// to dead clients if they exist, hence the while loop
	}	}	}
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  A client died.\n"];
		[logFile addLineToLogFile:tempString];
		[gameInfo dumpToLog:logFile];
		[tempString free];
	}
#endif
	return self;
}

- (oneway)clientCheckIn:(in id <HighScoreClient>)client
		// new client alerts of presence so that server
		// can notify client of changes in the table
{
	BOOL *auth = (BOOL *)malloc(sizeof(BOOL));
	NXConnection *conn;
	*auth = NO;
	if ([(NXProxy *)client isProxy]) // could be Object subclass, too.
			conn = [(NXProxy *)client connectionForProxy];
	else {	// client is local, so no connection, and it's authorized.
		conn = nil;
		*auth = YES;
	}
	// add the client to the list
	[clientList addObject:client];
	[connList addObject:conn];
	[clientAuth addElement:auth];
	[conn registerForInvalidationNotification:self];
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  A client checked in.\n"];
		[logFile addLineToLogFile:tempString];
		[gameInfo dumpToLog:logFile];
		[tempString free];
	}
#endif
	// ask for gameInfo object if we don't have it yet
	if (!gameInfo) [client sendGameInfoTo:gameName];
	else [client acceptTable:table name:gameName];
	return self;
}
					
- (oneway)addSlot:newSlot			// new high scores come in here
		fromClient:(in id <HighScoreClient>)client	// and go to all clients
{
	int c;
	id <HighScoreClient> tempClient; // gets rid of protocol warnings
	
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  A new slot was submitted.\n"];
		[logFile addLineToLogFile:tempString];
		[newSlot dumpToLog:logFile];
		[tempString free];
	}
	// insert the new score into the table; return if it doesn't fit
	if (![table addSlot:newSlot]) return self;
	// The server will send each client -addSlot:tableName: messages for every
	// slot which changes while the client is connected.  That way, the client
	// can update panels, etc. when someone else gets a new highscore.
	for (c=0; c<[clientList count]; c++) {
		tempClient = [clientList objectAt:c];
		if (client != tempClient) {
			[tempClient addSlot:newSlot tableName:gameName];
		}
	}
	// save the table to a file.  Done every time there's a change
	// so that nothing is lost if we crash, die, or get killed.
	[self save];
	return self;
}

- (BOOL)authorize:(id <HighScoreClient>)client
{	// get password from client and compare to gameInfo password via crypt()
	id password = [client password];	// returns a DAYString
	id encr = [password encrypt:[[gameInfo encryptedPassword] left:2]];
	BOOL ret = YES;
	if ([encr compareTo:[gameInfo encryptedPassword]]) ret = NO;
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Client sent password "];
		if (ret) [tempString cat:"successfully.\n"];
		else [tempString cat:"but it was wrong.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
	return ret;
}

- (BOOL)validateClient:(id <HighScoreClient>)client
{
	unsigned num = [clientList indexOf:client];
	BOOL *valid = [clientAuth elementAt:num];
	if (*valid != YES) return [self authorize:client];
	return YES;
}

- (oneway)clearTable:(in id <HighScoreClient>)sender
						// zero out the table.  Asks sender for
						// proper authentication first.  See the table
						// editing app for an example of how to do this.
{ // need to validate sender first off
	int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
	if (![self validateClient:sender]) return self;
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Client cleared the table.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
	// clear the table
	[table freeObjects];
	if (template) { // if available, use the empty "template"
		[table free];
		table = [template copy];
	}
	[self save];
	// now send it off to the clients
	for (c=0; c<[clientList count]; c++) {
		tempClient = [clientList objectAt:c];
		// ***** commented out since we want to update _all_ clients!
		//if (sender != tempClient) {
			[tempClient acceptTable:table name:gameName];
		//}
	}
	return self;
}

- (oneway)deleteSlot:(in int)i client:(in id <HighScoreClient>)sender
{
	id <HighScoreClient> tempClient; // gets rid of protocol warnings
	int c;
	if (![self validateClient:sender]) return self;
	if (loggingIsOn) {
		char *string = (char *)malloc(16);
		id tempString = [[gameName copy] cat:":  Client removed slot #"];
		sprintf(string, "%d\n", i);
		[tempString cat:string];
		[logFile addLineToLogFile:tempString];
		[tempString free];
		free(string);
	}
	[table removeObjectAt:i];
	for (c=0; c<[clientList count]; c++) {
		tempClient = [clientList objectAt:c];
		if (sender != [clientList objectAt:c]) {
			[tempClient removeSlotAt:i tableName:gameName];
		}
	}
	[self save];
	return self;
}

- (oneway)replaceSlot:(in int)i with:(bycopy in id)aSlot
		client:(in id <HighScoreClient>)sender
{
	int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
	if (![self validateClient:sender]) return self;
	if (loggingIsOn) {
		char *string = (char *)malloc(16);
		id tempString = [[gameName copy] cat:":  Client replaced slot #"];
		sprintf(string, "%d\n", i);
		[tempString cat:string];
		[logFile addLineToLogFile:tempString];
		[tempString free];
		free(string);
	}
	[table replaceObjectAt:i with:aSlot];
	for (c=0; c<[clientList count]; c++) {
		tempClient = [clientList objectAt:c];
		if (sender != tempClient) {
			[tempClient replaceSlotAt:i with:aSlot tableName:gameName];
		}
	}
	[self save];
	return self;
}

// ***** not yet implemented.  Will be used mostly by editors to guarantee
// edits properly sent to all clients.  For locking all other clients out
// of a table temporarily...
- (oneway)lockTable { return self; }
- (oneway)unlockTable { return self; }

- (const char *)gameName	// Name of the game we are serving
{ return [gameName stringValue]; }

- save		// flushes the table to the appropriate file.
{
    NXTypedStream *typedStream;
	haveNonTemplateTable = NO;
	NX_DURING	
	typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
			NX_WRITEONLY);
	NXWriteObject(typedStream, gameInfo);
	NXWriteObject(typedStream, table);
	NXWriteObject(typedStream, template);
	NXCloseTypedStream(typedStream);
	NX_HANDLER
		// deal with typed stream errors here *****
		fprintf(stderr, "Exception %d raised in -save.\n",	
				NXLocalHandler.code);
	NX_ENDHANDLER
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Saved table.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
#endif
	return self;
}

- _makeTableRatherThanLoad
{
	table = [template copy];
	if (!table) {
		haveNonTemplateTable = YES;
		table = [[HighScoreTable alloc] init]; // ***** should follow gameInfo params...
	}
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy]
				cat:":  Built table from template.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
#endif
	return self;
}

- load		// load the table from a file, if it exists.
{
    NXTypedStream *typedStream;
    FILE *testFile;
		
	haveNonTemplateTable = NO;
	// for some reason, NXOpenTypedStreamForFile() isn't returning
	// NULL for me when the file doesn't exist, so I check for the
	// file's existence first.
	testFile = fopen([scoreFile stringValue], "r");
	if (!testFile) return [self _makeTableRatherThanLoad];
	fclose(testFile);
	NX_DURING
	typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
			NX_READONLY);
	if (!typedStream) return [self _makeTableRatherThanLoad];
	
	if (table) [[table freeObjects] free];
	if (template) [[template freeObjects] free];
	gameInfo = NXReadObject(typedStream);
	// ***** should load dynamic classes here; GameInfo might require it!
	table = NXReadObject(typedStream);
	template = NXReadObject(typedStream);
	NXCloseTypedStream(typedStream);
	NX_HANDLER
		// deal with typed stream errors here *****
		[self _makeTableRatherThanLoad];
		fprintf(stderr, "Exception %d raised in -load.\n",	
				NXLocalHandler.code);
	NX_ENDHANDLER
#ifdef DEBUGLOG
	if (loggingIsOn) {
		id tempString = [[gameName copy] cat:":  Loaded table.\n"];
		[logFile addLineToLogFile:tempString];
		[tempString free];
	}
#endif
	return self;
}


@end