/*
 * qstat
 * by Steve Jankowski
 *
 * Doom3 / Quake4 protocol
 * Copyright 2005 Ludwig Nussel
 *
 * Licensed under the Artistic License, see LICENSE.txt for license terms
 */

#include <sys/types.h>
#ifndef _WIN32
 #include <sys/socket.h>
#endif
#include <stdio.h>
#include <stdlib.h>

#include "qstat.h"
#include "debug.h"
#include "assert.h"

static const char doom3_master_query[] = "\xFF\xFFgetServers\x00\x00\x00\x00\x00\x00";
//                                                  version ^^^^^^^^^^^^^^^^
//                                               null terminated mod string ^^^^
//                                                                   filterbyte ^^^^

static const char quake4_master_query[] = "\xFF\xFFgetServers\x00\x00\x00\x00\x00\x00\x00\x00";
//                                                   version ^^^^^^^^^^^^^^^^
//                                                null terminated mod string ^^^^
//                                                 null terminated player string ^^^^
//                                                       null terminated clan string ^^^^
//                                                                            filterbyte ^^^^

static unsigned
put_param_string(struct qserver *server, const char *paramname, char *buf, unsigned buflen, unsigned off)
{
	char *val = get_param_value(server, paramname, NULL);

	if (val && (strlen(val) < buflen - off - 2)) {
		strcpy(buf + off, val);
		off += strlen(val) + 1;
	} else {
		buf[off++] = '\0';
	}

	return (off);
}


static char *
build_doom3_masterfilter(struct qserver *server, char *buf, unsigned *buflen, int q4)
{
	int flen = 0;
	char *pkt, *r, *sep = "";
	unsigned char b = 0;
	char *proto = server->query_arg;
	unsigned ver;
	unsigned off = 13;

	if (!proto) {
		if (q4) {
			*buflen = sizeof(quake4_master_query);
			return ((char *)quake4_master_query);
		} else {
			*buflen = sizeof(doom3_master_query);
			return ((char *)doom3_master_query);
		}
	}

	ver = (atoi(proto) & 0xFFFF) << 16;
	proto = strchr(proto, '.');
	if (proto && *++proto) {
		ver |= (atoi(proto) & 0xFFFF);
	}
	if (q4) {
		ver |= 1 << 31; // third party flag
	}
	memcpy(buf, doom3_master_query, sizeof(doom3_master_query));
	put_long_little(ver, buf + off);
	off += 4;

	off = put_param_string(server, "game", buf, *buflen, off);

	if (q4) {
		off = put_param_string(server, "player", buf, *buflen, off);
		off = put_param_string(server, "clan", buf, *buflen, off);
	}

	pkt = get_param_value(server, "status", NULL);
	r = pkt;
	while (pkt && sep) {
		sep = strchr(r, ':');
		if (sep) {
			flen = sep - r;
		} else {
			flen = strlen(r);
		}

		if (strncmp(r, "password", flen) == 0) {
			b |= 0x01;
		} else if (strncmp(r, "nopassword", flen) == 0) {
			b |= 0x02;
		} else if (strncmp(r, "notfull", flen) == 0) {
			b |= 0x04;
		} else if (strncmp(r, "notfullnotempty", flen) == 0) {
			b |= 0x08;
		}
		r = sep + 1;
	}

	pkt = get_param_value(server, "gametype", NULL);
	if (pkt) {
		if (strncmp(pkt, "dm", flen) == 0) {
			b |= 0x10;
		} else if (strncmp(pkt, "tourney", flen) == 0) {
			b |= 0x20;
		} else if (strncmp(pkt, "tdm", flen) == 0) {
			b |= 0x30;
		}
	}

	buf[off++] = (char)b;
	*buflen = off;

	return (buf);
}


query_status_t
send_doom3master_request_packet(struct qserver *server)
{
	int rc = 0;
	int packet_len = -1;
	char *packet = NULL;
	char query_buf[4096] = { 0 };

	server->next_player_info = NO_PLAYER_INFO;

	packet_len = sizeof(query_buf);
	packet = build_doom3_masterfilter(server, query_buf, (unsigned *)&packet_len, 0);

	rc = send(server->fd, packet, packet_len, 0);
	if (rc == SOCKET_ERROR) {
		return (send_error(server, rc));
	}

	if (server->retry1 == n_retries) {
		gettimeofday(&server->packet_time1, NULL);
		server->n_requests++;
	} else {
		server->n_retries++;
	}
	server->retry1--;
	server->n_packets++;

	return (INPROGRESS);
}


query_status_t
send_quake4master_request_packet(struct qserver *server)
{
	int rc = 0;
	int packet_len = -1;
	char *packet = NULL;
	char query_buf[4096] = { 0 };

	server->next_player_info = NO_PLAYER_INFO;

	packet_len = sizeof(query_buf);
	packet = build_doom3_masterfilter(server, query_buf, (unsigned *)&packet_len, 1);

	rc = send(server->fd, packet, packet_len, 0);
	if (rc == SOCKET_ERROR) {
		return (send_error(server, rc));
	}

	if (server->retry1 == n_retries) {
		gettimeofday(&server->packet_time1, NULL);
		server->n_requests++;
	} else {
		server->n_retries++;
	}
	server->retry1--;
	server->n_packets++;

	return (INPROGRESS);
}


static const char doom3_masterresponse[] = "\xFF\xFFservers";

query_status_t
deal_with_doom3master_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	char *pkt, *dest;
	int len;

	server->ping_total += time_delta(&packet_recv_time, &server->packet_time1);

	if ((pktlen < sizeof(doom3_masterresponse) + 6) ||                                                     // at least one server
	    (pktlen - sizeof(doom3_masterresponse)) % 6 ||
	    (memcmp(doom3_masterresponse, rawpkt, sizeof(doom3_masterresponse)) != 0)) {
		server->server_name = SERVERERROR;
		server->master_pkt_len = 0;
		malformed_packet(server, "too short or invalid response");
		return (PKT_ERROR);
	}

	server->retry1 = 0;     // received at least one packet so no need to retry

	pkt = rawpkt + sizeof(doom3_masterresponse);
	len = pktlen - sizeof(doom3_masterresponse);

	server->master_pkt = (char *)realloc(server->master_pkt, server->master_pkt_len + len);

	dest = server->master_pkt + server->master_pkt_len;

	server->master_pkt_len += len;

	while (len > 0) {
		memcpy(dest, pkt, 4);
		dest[4] = pkt[5];
		dest[5] = pkt[4];
		dest += 6;
		pkt += 6;
		len -= 6;
	}
	assert(len == 0);

	server->n_servers = server->master_pkt_len / 6;

	debug(2, "%d servers added", server->n_servers);

	return (INPROGRESS);
}


static const char doom3_inforesponse[] = "\xFF\xFFinfoResponse";
static unsigned MAX_DOOM3_ASYNC_CLIENTS = 32;
static unsigned MAX_WOLF_ASYNC_CLIENTS = 16;

static query_status_t
_deal_with_doom3_packet(struct qserver *server, char *rawpkt, int pktlen, unsigned version)
{
	char *ptr = rawpkt;
	char *end = rawpkt + pktlen;
	int type = 0;
	int size = 0;
	int tail_size = 4;
	int viewers = 0;
	int tv = 0;
	unsigned num_players = 0;
	unsigned challenge = 0;
	unsigned protocolver = 0;
	char tmp[32];

	server->n_servers++;
	if (server->server_name == NULL) {
		server->ping_total += time_delta(&packet_recv_time, &server->packet_time1);
	} else {
		gettimeofday(&server->packet_time1, NULL);
	}

	// Check if correct reply
	if ((pktlen < sizeof(doom3_inforesponse) + 4 + 4 + 1) ||
	    (memcmp(doom3_inforesponse, ptr, sizeof(doom3_inforesponse)) != 0)) {
		malformed_packet(server, "too short or invalid response");
		return (PKT_ERROR);
	}
	ptr += sizeof(doom3_inforesponse);

	if (5 == version) {
		// TaskID
		ptr += 4;
		// osmask + ranked
		tail_size++;
	}

	challenge = swap_long_from_little(ptr);
	ptr += 4;

	protocolver = swap_long_from_little(ptr);
	ptr += 4;

	snprintf(tmp, sizeof(tmp), "%u.%u", protocolver >> 16, protocolver & 0xFFFF);
	debug(2, "challenge: 0x%08X, protocol: %s (0x%X)", challenge, tmp, protocolver);

	server->protocol_version = protocolver;
	add_rule(server, "protocol", tmp, NO_FLAGS);

	if (5 == version) {
		// Size Long
		size = swap_long_from_little(ptr);
		debug(2, "Size = %d", size);
		ptr += 4;
	}

// Commented out until we have a better way to specify the expected version
// This is due to prey demo requiring version 4 yet prey retail version 3

/*
 *  if( protocolver >> 16 != version )
 *  {
 *      malformed_packet(server, "protocol version %u, expected %u", protocolver >> 16, version );
 *      return PKT_ERROR;
 *  }
 */

	while (ptr < end) {
		// server info:
		// name value pairs null seperated
		// empty name && value signifies the end of section
		char *key, *val;
		unsigned keylen, vallen;

		key = ptr;
		ptr = memchr(ptr, '\0', end - ptr);
		if (!ptr) {
			malformed_packet(server, "no rule key");
			return (PKT_ERROR);
		}
		keylen = ptr - key;

		val = ++ptr;
		ptr = memchr(ptr, '\0', end - ptr);
		if (!ptr) {
			malformed_packet(server, "no rule value");
			return (PKT_ERROR);
		}
		vallen = ptr - val;
		++ptr;

		if ((keylen == 0) && (vallen == 0)) {
			type = 1;
			break;  // end
		}

		debug(2, "key:%s=%s:", key, val);

		// Lets see what we've got
		if (0 == strcasecmp(key, "si_name")) {
			server->server_name = strdup(val);
		} else if (0 == strcasecmp(key, "fs_game")) {
			server->game = strdup(val);
		}
#if 0
			else if (0 == strcasecmp(key, "si_version")) {
				// format:
				x
				// DOOM 1.0.1262.win-x86 Jul  8 2004 16:46:37
				server->protocol_version = atoi(val + 1);
			}
#endif
		else if (0 == strcasecmp(key, "si_map")) {
			if ((5 == version) || (6 == version)) {
				// ET:QW reports maps/<mapname>.entities
				// so we strip that here if it exists
				char *tmpp = strstr(val, ".entities");
				if (NULL != tmpp) {
					*tmpp = '\0';
				}

				if (0 == strncmp(val, "maps/", 5)) {
					val += 5;
				}
			}
			server->map_name = strdup(val);
		} else if (0 == strcasecmp(key, "si_maxplayers")) {
			server->max_players = atoi(val);
		} else if (0 == strcasecmp(key, "ri_maxViewers")) {
			char max[20];
			sprintf(max, "%d", server->max_players);
			add_rule(server, "si_maxplayers", max, NO_FLAGS);
			server->max_players = atoi(val);
		} else if (0 == strcasecmp(key, "ri_numViewers")) {
			viewers = atoi(val);
			tv = 1;
		}

		add_rule(server, key, val, NO_FLAGS);
	}

	if (type != 1) {
		// no more info should be player headers here as we
		// requested it
		malformed_packet(server, "player info missing");
		return (PKT_ERROR);
	}

	// now each player details
	while (ptr < end - tail_size) {
		struct player *player;
		char *val;
		unsigned char player_id = *ptr++;
		short ping = 0;
		unsigned rate = 0;

		if (((6 == version) && (MAX_WOLF_ASYNC_CLIENTS == player_id)) || (MAX_DOOM3_ASYNC_CLIENTS == player_id)) {
			break;
		}
		debug(2, "ID = %d\n", player_id);

		// Note: id's are not steady
		if (ptr + 7 > end) {// 2 ping + 4 rate + empty player name ('\0')
			// run off the end and shouldnt have
			malformed_packet(server, "player info too short");
			return (PKT_ERROR);
		}

/*
 *      if ( 6 == version )
 *      {
 *          // Playerid is broken in wolf its always 0
 *          player_id = num_players;
 *      }
 */

		player = add_player(server, player_id);
		if (!player) {
			malformed_packet(server, "duplicate player id");
			return (PKT_ERROR);
		}

		// doesnt support score so set a sensible default
		player->score = 0;
		player->frags = 0;

		// Ping
		ping = swap_short_from_little(ptr);
		player->ping = ping;
		ptr += 2;

		if ((5 != version) || (0xa0013 >= protocolver)) {   // No Rate in ETQW since v1.4 ( protocol v10.19 )
			// Rate
			rate = swap_long_from_little(ptr);
			{
				char buf[16];
				snprintf(buf, sizeof(buf), "%u", rate);
				player_add_info(player, "rate", buf, NO_FLAGS);
			}
			ptr += 4;
		}

		if ((5 == version) && (((0xd0009 == protocolver) || (0xd000a == protocolver)) && (0 != num_players))) {         // v13.9 or v13.10
			// Fix the packet offset due to the single bit used for bot
			// which realigns at the byte boundary for the player name
			ptr++;
		}

		// Name
		val = ptr;
		ptr = memchr(ptr, '\0', end - ptr);
		if (!ptr) {
			malformed_packet(server, "player name not null terminated");
			return (PKT_ERROR);
		}
		player->name = strdup(val);
		ptr++;

		switch (version) {
		case 2: // Quake 4
			val = ptr;
			ptr = memchr(ptr, '\0', end - ptr);
			if (!ptr) {
				malformed_packet(server, "player clan not null terminated");
				return (PKT_ERROR);
			}
			player->tribe_tag = strdup(val);
			ptr++;
			debug(2, "Player[%d] = %s, ping %hu, rate %u, id %hhu, clan %s",
			    num_players, player->name, ping, rate, player_id, player->tribe_tag);
			break;

		case 5:                                 // ETQW
		case 6:                                 // Wolfenstien
			if (0xa0011 <= protocolver) {   // clan tag since 10.17
				// clantag position
				ptr++;

				// clantag
				val = ptr;
				ptr = memchr(ptr, '\0', end - ptr);
				if (!ptr) {
					malformed_packet(server, "player clan not null terminated");
					return (PKT_ERROR);
				}
				player->tribe_tag = strdup(val);
				ptr++;
			}

			// Bot flag
			if ((0xd0009 == protocolver) || (0xd000a == protocolver)) { // v13.9 or v13.10
				// Bot flag is a single bit so need to realign everything from here on in :(
				int i;
				unsigned char *tp = (unsigned char *)ptr;
				player->type_flag = (*tp) << 7;

				// alignment is reset at the end
				for (i = 0; i < 8 && tp < (unsigned char *)end; i++) {
					*tp = (*tp) >> 1 | *(tp + 1) << 7;
					tp++;
				}
			} else {
				player->type_flag = *ptr++;
			}

			if (0xa0011 <= protocolver) {   // clan tag since 10.17
				debug(2, "Player[%d] = %s, ping %hu, rate %u, id %hhu, bot %hu, clan %s",
				    num_players, player->name, ping, rate, player_id, player->type_flag, player->tribe_tag);
			} else {
				debug(2, "Player[%d] = %s, ping %hu, rate %u, id %hhu, bot %hu",
				    num_players, player->name, ping, rate, player_id, player->type_flag);
			}
			break;

		default:
			debug(2, "Player[%d] = %s, ping %hu, rate %u, id %hhu", num_players, player->name, ping, rate, player_id);
		}

		++num_players;
	}

	if (end - ptr >= 4) {
		// OS Mask
		snprintf(tmp, sizeof(tmp), "0x%X", swap_long_from_little(ptr));
		add_rule(server, "osmask", tmp, NO_FLAGS);
		debug(2, "osmask %s", tmp);
		ptr += 4;

		if ((5 == version) && (end - ptr >= 1)) {
			// Ranked flag
			snprintf(tmp, sizeof(tmp), "%hhu", *ptr++);
			add_rule(server, "ranked", tmp, NO_FLAGS);

			if (end - ptr >= 5) {
				// Time Left
				snprintf(tmp, sizeof(tmp), "%d", swap_long_from_little(ptr));
				add_rule(server, "timeleft", tmp, NO_FLAGS);
				ptr += 4;

				// Game State
				snprintf(tmp, sizeof(tmp), "%hhu", *ptr++);
				add_rule(server, "gamestate", tmp, NO_FLAGS);

				if (end - ptr >= 1) {
					// Server Type
					unsigned char servertype = *ptr++;
					snprintf(tmp, sizeof(tmp), "%hhu", servertype);
					add_rule(server, "servertype", tmp, NO_FLAGS);

					switch (servertype) {
					case 0: // Regular Server
						// Interested Clients
						snprintf(tmp, sizeof(tmp), "%hhu", *ptr++);
						add_rule(server, "interested_clients", tmp, NO_FLAGS);
						break;

					case 1: // TV Server
						// Connected Clients
						snprintf(tmp, sizeof(tmp), "%d", swap_long_from_little(ptr));
						ptr += 4;
						add_rule(server, "interested_clients", tmp, NO_FLAGS);

						// Max Clients
						snprintf(tmp, sizeof(tmp), "%d", swap_long_from_little(ptr));
						ptr += 4;
						add_rule(server, "interested_clients", tmp, NO_FLAGS);
						break;

					default:
						// Unknown
						if (show_errors) {
							fprintf(stderr, "Unknown server type %d\n", servertype);
						}
					}
				}
			}
		}
	} else {
		malformed_packet(server, "osmask missing");
	}

#if 0
		if (end - ptr) {
			malformed_packet(server, "%ld byes left", end - ptr);
		}
#endif

	if (0 == tv) {
		debug(2, "Num players = %d", num_players);
		server->num_players = num_players;
	} else {
		server->num_players = viewers;
	}

	return (DONE_FORCE);
}


query_status_t
deal_with_doom3_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 1));
}


query_status_t
deal_with_quake4_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 2));
}


query_status_t
deal_with_prey_demo_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 4));
}


query_status_t
deal_with_prey_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 3));
}


query_status_t
deal_with_etqw_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 5));
}


query_status_t
deal_with_wolf_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	return (_deal_with_doom3_packet(server, rawpkt, pktlen, 6));
}
