/*
 * qstat
 * by Steve Jankowski
 *
 * Frontlines-Fuel of War protocol
 * Copyright 2008 Steven Hartland
 *
 * Licensed under the Artistic License, see LICENSE.txt for license terms
 *
 */

#include <sys/types.h>
#ifndef _WIN32
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
#else
 #include <winsock.h>
#endif

#include <stdlib.h>
#include <stdio.h>

#include "debug.h"
#include "qstat.h"
#include "packet_manip.h"

#define FL_GETCHALLENGE		"\xFF\xFF\xFF\xFF\x57"
#define FL_CHALLENGERESPONSE	0x41
#define FL_INFO			"\xFF\xFF\xFF\xFF\x46LSQ"
#define FL_INFORESPONSE		0x49
#define FL_PLAYER		"\xFF\xFF\xFF\xFF\x55"
#define FL_PLAYERRESPONSE	0x44
#define FL_RULES		"\xFF\xFF\xFF\xFF\x56"
#define FL_RULESRESPONSE	0x45

struct fl_status {
	unsigned sent_challenge : 1;
	unsigned have_challenge : 1;
	unsigned sent_info : 1;
	unsigned have_info : 1;
	unsigned sent_player : 1;
	unsigned have_player : 1;
	unsigned sent_rules : 1;
	unsigned have_rules : 1;
	unsigned challenge;
	unsigned char type;
};

query_status_t
send_fl_request_packet(struct qserver *server)
{
	struct fl_status *status = (struct fl_status *)server->master_query_tag;

	if (SOCKET_ERROR == qserver_send_initial(server, FL_INFO, sizeof(FL_INFO) - 1)) {
		return (SOCKET_ERROR);
	}

	status->sent_info = 1;
	status->type = 0;

	return (INPROGRESS);
}


query_status_t
send_fl_rule_request_packet(struct qserver *server)
{
	struct fl_status *status = (struct fl_status *)server->master_query_tag;

	if (1 >= status->type) {
		// Not supported
		return (PKT_ERROR);
	}

	if (!get_server_rules && !get_player_info) {
		return (DONE_FORCE);
	}

	do {
		if (!status->have_challenge) {
			debug(3, "sending challenge");
			if (SOCKET_ERROR == qserver_send_initial(server, FL_GETCHALLENGE, sizeof(FL_GETCHALLENGE) - 1)) {
				return (SOCKET_ERROR);
			}
			status->sent_challenge = 1;
			break;
		} else if (get_server_rules && !status->have_rules) {
			char buf[sizeof(FL_RULES) - 1 + 4] = FL_RULES;
			memcpy(buf + sizeof(FL_RULES) - 1, &status->challenge, 4);
			debug(3, "sending rule query");
			if (SOCKET_ERROR == qserver_send_initial(server, buf, sizeof(buf))) {
				return (SOCKET_ERROR);
			}
			status->sent_rules = 1;
			break;
		} else if (get_player_info && !status->have_player) {
			char buf[sizeof(FL_PLAYER) - 1 + 4] = FL_PLAYER;
			memcpy(buf + sizeof(FL_PLAYER) - 1, &status->challenge, 4);
			debug(3, "sending player query");
			if (SOCKET_ERROR == qserver_send_initial(server, buf, sizeof(buf))) {
				return (SOCKET_ERROR);
			}
			status->sent_player = 1;
			break;
		} else {
			debug(3, "timeout");
			// we are probably called due to timeout, restart.
			status->have_challenge = 0;
			status->have_rules = 0;
		}
	} while (1);

	return (INPROGRESS);
}


query_status_t
deal_with_fl_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	struct fl_status *status = (struct fl_status *)server->master_query_tag;
	char *pkt = rawpkt;
	char buf[16];
	char *str;
	unsigned cnt;
	unsigned short tmp_short;

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

	if (pktlen < 5) {
		goto out_too_short;
	}

	if (0 == memcmp(pkt, "\xFF\xFF\xFF\xFE", 4)) {
		// fragmented packet
		unsigned char pkt_index, pkt_max;
		unsigned int pkt_id;
		SavedData *sdata;

		if (pktlen < 9) {
			goto out_too_short;
		}

		// format:
		// int Header
		// int RequestId
		// byte PacketNumber
		// byte NumPackets
		// Short SizeOfPacketSplits

		// Header
		pkt += 4;

		// RequestId
		pkt_id = ntohl(*(long *)pkt);
		debug(3, "RequestID: %d", pkt_id);
		pkt += 4;

		// The next two bytes are:
		// 1. the max packets sent ( byte )
		// 2. the index of this packet starting from 0 ( byte )
		// 3. Size of the split ( short )
		if (pktlen < 10) {
			goto out_too_short;
		}

		// PacketNumber
		pkt_index = ((unsigned char)*pkt);

		// NumPackates
		pkt_max = ((unsigned char)*(pkt + 1));

		// SizeOfPacketSplits
		debug(3, "packetid[2]: 0x%hhx => idx: %hhu, max: %hhu", *pkt, pkt_index, pkt_max);
		pkt += 4;
		pktlen -= 12;

		// pkt_max is the total number of packets expected
		// pkt_index is a bit mask of the packets received.
		if (server->saved_data.data == NULL) {
			sdata = &server->saved_data;
		} else {
			sdata = (SavedData *)calloc(1, sizeof(SavedData));
			sdata->next = server->saved_data.next;
			server->saved_data.next = sdata;
		}

		sdata->pkt_index = pkt_index;
		sdata->pkt_max = pkt_max;
		sdata->pkt_id = pkt_id;
		sdata->datalen = pktlen;
		sdata->data = (char *)malloc(pktlen);
		if (NULL == sdata->data) {
			malformed_packet(server, "Out of memory");
			return (MEM_ERROR);
		}

		memcpy(sdata->data, pkt, sdata->datalen);

		// combine_packets will call us recursively
		return (combine_packets(server));
	} else if (0 != memcmp(pkt, "\xFF\xFF\xFF\xFF", 4)) {
		malformed_packet(server, "invalid packet header");
		return (PKT_ERROR);
	}

	pkt += 4;
	pktlen -= 4;

	pktlen -= 1;
	debug(2, "FL type = 0x%x", *pkt);
	switch (*pkt++) {
	case FL_CHALLENGERESPONSE:
		if (pktlen < 4) {
			goto out_too_short;
		}
		memcpy(&status->challenge, pkt, 4);
		// do not count challenge as retry
		if (!status->have_challenge && (server->retry1 != n_retries)) {
			++server->retry1;
			if (server->n_retries) {
				--server->n_retries;
			}
		}
		status->have_challenge = 1;
		debug(3, "challenge %x", status->challenge);
		break;

	case FL_INFORESPONSE:
		if (pktlen < 1) {
			goto out_too_short;
		}
		status->type = *pkt;
		if ((*pkt > 1) && (get_server_rules || get_player_info)) {
			server->next_rule = ""; // trigger calling send_fl_rule_request_packet
		}
		snprintf(buf, sizeof(buf), "%hhX", *pkt);
		add_rule(server, "protocol", buf, 0);
		pktlen--;
		pkt++;

		// ServerName
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		server->server_name = strdup(pkt);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		// MapName
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		server->map_name = strdup(pkt);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		// ModName
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		server->game = strdup(pkt);
		add_rule(server, "modname", pkt, 0);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		// GameMode
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		add_rule(server, "gamemode", pkt, 0);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		// GameDescription
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		add_rule(server, "gamedescription", pkt, 0);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		// GameVersion
		str = memchr(pkt, '\0', pktlen);
		if (!str) {
			goto out_too_short;
		}
		add_rule(server, "gameversion", pkt, 0);
		pktlen -= str - pkt + 1;
		pkt += str - pkt + 1;

		if (pktlen < 13) {
			goto out_too_short;
		}

		// GamePort
		tmp_short = ((unsigned short)pkt[0] << 8) | ((unsigned short)pkt[1]);
		change_server_port(server, tmp_short, 0);
		pkt += 2;

		// Num Players
		server->num_players = (unsigned char)*pkt++;

		// Max Players
		server->max_players = (unsigned char)*pkt++;

		// Dedicated
		add_rule(server, "dedicated", ('d' == *pkt++) ? "1" : "0", 0);

		// OS
		switch (*pkt) {
		case 'l':
			add_rule(server, "sv_os", "linux", 0);
			break;

		case 'w':
			add_rule(server, "sv_os", "windows", 0);
			break;

		default:
			buf[0] = *pkt;
			buf[1] = '\0';
			add_rule(server, "sv_os", buf, 0);
			break;
		}
		pkt++;

		// Passworded
		add_rule(server, "passworded", (*pkt++) ? "1" : "0", 0);

		// Anticheat
		add_rule(server, "passworded", (*pkt++) ? "1" : "0", 0);

		// FrameTime
		sprintf(buf, "%hhu", *pkt++);
		add_rule(server, "frametime", buf, 0);

		// Round
		sprintf(buf, "%hhu", *pkt++);
		add_rule(server, "round", buf, 0);

		// RoundMax
		sprintf(buf, "%hhu", *pkt++);
		add_rule(server, "roundmax", buf, 0);

		// RoundSeconds
		tmp_short = ((unsigned short)pkt[0] << 8) | ((unsigned short)pkt[1]);
		sprintf(buf, "%hu", tmp_short);
		add_rule(server, "roundseconds", buf, 0);
		pkt += 2;

		status->have_info = 1;

		server->retry1 = n_retries;

		server->next_player_info = server->num_players;

		break;

	case FL_RULESRESPONSE:
		if (pktlen < 2) {
			goto out_too_short;
		}

		cnt = ((unsigned char)pkt[0] << 8) + ((unsigned char)pkt[1]);
		pktlen -= 2;
		pkt += 2;

		debug(3, "num_rules: %d", cnt);

		for ( ; cnt && pktlen > 0; --cnt) {
			char *key, *value;
			str = memchr(pkt, '\0', pktlen);
			if (!str) {
				break;
			}
			key = pkt;
			pktlen -= str - pkt + 1;
			pkt += str - pkt + 1;

			str = memchr(pkt, '\0', pktlen);
			if (!str) {
				break;
			}
			value = pkt;
			pktlen -= str - pkt + 1;
			pkt += str - pkt + 1;

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

		if (cnt) {
			malformed_packet(server, "packet contains too few rules, missing %d", cnt);
			server->missing_rules = 1;
		}
		if (pktlen) {
			malformed_packet(server, "garbage at end of rules, %d bytes left", pktlen);
		}

		status->have_rules = 1;

		server->retry1 = n_retries;

		break;

	case FL_PLAYERRESPONSE:

		if (pktlen < 1) {
			goto out_too_short;
		}

		cnt = (unsigned char)pkt[0];
		pktlen -= 1;
		pkt += 1;

		debug(3, "num_players: %d", cnt);

		for ( ; cnt && pktlen > 0; --cnt) {
			unsigned idx;
			const char *name;
			struct player *p;

			// Index
			idx = *pkt++;
			--pktlen;

			// PlayerName
			str = memchr(pkt, '\0', pktlen);
			if (!str) {
				break;
			}
			name = pkt;
			pktlen -= str - pkt + 1;
			pkt += str - pkt + 1;

			if (pktlen < 8) {
				goto out_too_short;
			}

			debug(3, "player index %d", idx);
			p = add_player(server, server->n_player_info);
			if (p) {
				union {
					int i;
					float fl;
				}
				temp;

				p->name = strdup(name);

				// Score
				p->frags = ntohl(*(unsigned int *)pkt);

				// TimeConnected
				temp.i = ntohl(*(unsigned int *)(pkt + 4));
				p->connect_time = temp.fl;

				// Ping
				p->ping = 0;
				p->ping = ntohs(*(unsigned int *)(pkt + 8));
				//((unsigned char*)&p->ping)[0] = pkt[9];
				//((unsigned char*)&p->ping)[1] = pkt[8];

				// ProfileId
				//p->profileid = ntohl( *(unsigned int *)pkt+10 );

				// Team
				p->team = *(pkt + 14);
			}
			pktlen -= 15;
			pkt += 15;
		}

		if (pktlen) {
			malformed_packet(server, "garbage at end of player info, %d bytes left", pktlen);
		}

		status->have_player = 1;

		server->retry1 = n_retries;

		break;

	default:
		malformed_packet(server, "invalid packet id %hhx", *--pkt);
		return (PKT_ERROR);
	}

	if (
		(!get_player_info || (get_player_info && status->have_player)) &&
		(!get_server_rules || (get_server_rules && status->have_rules))
		) {
		server->next_rule = NULL;
	}

	return (DONE_AUTO);

out_too_short:
	malformed_packet(server, "packet too short");

	return (PKT_ERROR);
}


// vim: sw=4 ts=4 noet
