// serverbrowser.cpp: eihrul's concurrent resolver, and server browser window
// management

#include "SDL_thread.h"
#include "cube.h"

#import "Command.h"
#import "ResolverResult.h"
#import "ResolverThread.h"
#import "ServerInfo.h"

static OFMutableArray<ResolverThread *> *resolverthreads;
OFMutableArray<OFString *> *resolverqueries;
OFMutableArray<ResolverResult *> *resolverresults;
SDL_sem *resolversem;
static int resolverlimit = 1000;

void
resolverinit(int threads, int limit)
{
	resolverthreads = [[OFMutableArray alloc] init];
	resolverqueries = [[OFMutableArray alloc] init];
	resolverresults = [[OFMutableArray alloc] init];
	resolverlimit = limit;
	resolversem = SDL_CreateSemaphore(0);

	while (threads > 0) {
		ResolverThread *rt = [ResolverThread thread];
		rt.name = @"resolverthread";
		[resolverthreads addObject:rt];
		[rt start];
		--threads;
	}
}

void
resolverstop(size_t i, bool restart)
{
	@synchronized(ResolverThread.class) {
		ResolverThread *rt = resolverthreads[i];
		[rt stop];

		if (restart) {
			rt = [ResolverThread thread];
			rt.name = @"resolverthread";

			resolverthreads[i] = rt;

			[rt start];
		} else
			[resolverthreads removeObjectAtIndex:i];
	}
}

void
resolverclear()
{
	@synchronized(ResolverThread.class) {
		[resolverqueries removeAllObjects];
		[resolverresults removeAllObjects];

		while (SDL_SemTryWait(resolversem) == 0)
			;

		for (size_t i = 0; i < resolverthreads.count; i++)
			resolverstop(i, true);
	}
}

void
resolverquery(OFString *name)
{
	@synchronized(ResolverThread.class) {
		[resolverqueries addObject:name];
		SDL_SemPost(resolversem);
	}
}

bool
resolvercheck(OFString **name, ENetAddress *address)
{
	@synchronized(ResolverThread.class) {
		if (resolverresults.count > 0) {
			ResolverResult *rr = resolverresults.lastObject;
			*name = rr.query;
			*address = rr.address;
			[resolverresults removeLastObject];
			return true;
		}

		for (size_t i = 0; i < resolverthreads.count; i++) {
			ResolverThread *rt = resolverthreads[i];

			if (rt.query) {
				if (lastmillis - rt.starttime > resolverlimit) {
					resolverstop(i, true);
					*name = rt.query;
					return true;
				}
			}
		}
	}

	return false;
}

static OFMutableArray<ServerInfo *> *servers;
static ENetSocket pingsock = ENET_SOCKET_NULL;
static int lastinfo = 0;

OFString *
getservername(int n)
{
	return servers[n].name;
}

void
addserver(OFString *servername)
{
	for (ServerInfo *si in servers)
		if ([si.name isEqual:servername])
			return;

	if (servers == nil)
		servers = [[OFMutableArray alloc] init];

	[servers addObject:[ServerInfo infoWithName:servername]];
}

COMMAND(addserver, ARG_1STR, ^(OFString *servername) {
	addserver(servername);
})

void
pingservers()
{
	ENetBuffer buf;
	unsigned char ping[MAXTRANS];
	unsigned char *p;

	for (ServerInfo *si in servers) {
		if (si.address.host == ENET_HOST_ANY)
			continue;

		p = ping;
		putint(&p, lastmillis);
		buf.data = ping;
		buf.dataLength = p - ping;
		ENetAddress address = si.address;
		enet_socket_send(pingsock, &address, &buf, 1);
	}

	lastinfo = lastmillis;
}

void
checkresolver()
{
	OFString *name = nil;
	ENetAddress addr = { ENET_HOST_ANY, CUBE_SERVINFO_PORT };
	while (resolvercheck(&name, &addr)) {
		if (addr.host == ENET_HOST_ANY)
			continue;

		for (ServerInfo *si in servers) {
			if ([name isEqual:si.name]) {
				si.address = addr;
				addr.host = ENET_HOST_ANY;
				break;
			}
		}
	}
}

void
checkpings()
{
	enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
	ENetBuffer buf;
	ENetAddress addr;
	unsigned char ping[MAXTRANS], *p;
	char text[MAXTRANS];
	buf.data = ping;
	buf.dataLength = sizeof(ping);

	while (enet_socket_wait(pingsock, &events, 0) >= 0 && events) {
		if (enet_socket_receive(pingsock, &addr, &buf, 1) <= 0)
			return;

		for (ServerInfo *si in servers) {
			if (addr.host == si.address.host) {
				p = ping;
				si.ping = lastmillis - getint(&p);
				si.protocol = getint(&p);
				if (si.protocol != PROTOCOL_VERSION)
					si.ping = 9998;
				si.mode = getint(&p);
				si.numplayers = getint(&p);
				si.minremain = getint(&p);
				sgetstr();
				si.map = @(text);
				sgetstr();
				si.sdesc = @(text);
				break;
			}
		}
	}
}

void
refreshservers()
{
	checkresolver();
	checkpings();
	if (lastmillis - lastinfo >= 5000)
		pingservers();
	[servers sort];

	__block int maxmenu = 16;
	[servers enumerateObjectsUsingBlock:^(
	    ServerInfo *si, size_t i, bool *stop) {
		if (si.address.host != ENET_HOST_ANY && si.ping != 9999) {
			if (si.protocol != PROTOCOL_VERSION)
				si.full = [OFString stringWithFormat:
				        @"%@ [different cube protocol]",
				    si.name];
			else
				si.full = [OFString
				    stringWithFormat:@"%d\t%d\t%@, %@: %@ %@",
				    si.ping, si.numplayers,
				    si.map.length > 0 ? si.map : @"[unknown]",
				    modestr(si.mode), si.name, si.sdesc];
		} else
			si.full = [OFString stringWithFormat:
			        (si.address.host != ENET_HOST_ANY
			                ? @"%@ [waiting for server response]"
			                : @"%@ [unknown host]\t"),
			    si.name];

		// cut off too long server descriptions
		if (si.full.length > 50)
			si.full = [si.full substringToIndex:50];

		menumanual(1, i, si.full);

		if (!--maxmenu)
			return;
	}];
}

static void
servermenu()
{
	if (pingsock == ENET_SOCKET_NULL) {
		pingsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM, NULL);
		resolverinit(1, 1000);
	}

	resolverclear();

	for (ServerInfo *si in servers)
		resolverquery(si.name);

	refreshservers();
	menuset(1);
}

COMMAND(servermenu, ARG_NONE, ^{
	servermenu();
})

COMMAND(updatefrommaster, ARG_NONE, ^{
	const int MAXUPD = 32000;
	unsigned char buf[MAXUPD];
	unsigned char *reply = retrieveservers(buf, MAXUPD);
	if (!*reply || strstr((char *)reply, "<html>") ||
	    strstr((char *)reply, "<HTML>"))
		conoutf(@"master server not replying");
	else {
		[servers removeAllObjects];
		execute(@((char *)reply), true);
	}
	servermenu();
})

void
writeservercfg()
{
	FILE *f = fopen("servers.cfg", "w");
	if (!f)
		return;
	fprintf(f, "// servers connected to are added here automatically\n\n");
	for (ServerInfo *si in servers.reversedArray)
		fprintf(f, "addserver %s\n", si.name.UTF8String);
	fclose(f);
}
