/*
 * Copyright (C) Volition, Inc. 2005.  All rights reserved.
 * 
 * All source code herein is the property of Volition, Inc. You may not sell 
 * or otherwise commercially exploit the source or things you created based on the 
 * source.
 *
*/


#ifndef _WIN32
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <netdb.h>
#endif

#ifdef SCP_BSD
#include <sys/socket.h>
#endif

#include "globalincs/pstypes.h"
#include "io/timer.h"
#include "network/multi.h"
#include "network/multi_pxo.h"
#include "network/gtrack.h"
#include "network/ptrack.h"
#include "network/multi_fstracker.h"


// check structs for size compatibility
SDL_COMPILE_TIME_ASSERT(game_packet_header, sizeof(game_packet_header) == 1529);
SDL_COMPILE_TIME_ASSERT(freespace2_net_game_data, sizeof(freespace2_net_game_data) == 120);
SDL_COMPILE_TIME_ASSERT(game_list_ip4, sizeof(game_list_ip4) == 384);
SDL_COMPILE_TIME_ASSERT(game_list_ip6, sizeof(game_list_ip6) == 504);
SDL_COMPILE_TIME_ASSERT(in6_addr, sizeof(in6_addr) == 16);
SDL_COMPILE_TIME_ASSERT(filter_game_list_struct, sizeof(filter_game_list_struct) == 40);



//Variables
static SOCKADDR_STORAGE gtrackaddr;

static game_list GameBuffer[MAX_GAME_BUFFERS];
static int GameType;//d3 or fs

static time_t LastTrackerUpdate;
static unsigned int LastSentToTracker;
static unsigned int TrackerAckdUs;
static unsigned int TrackerGameIsRunning;

static game_packet_header TrackerGameData;
static game_packet_header GameListReq;
static game_packet_header TrackAckPacket;
static game_packet_header GameOverPacket;

static freespace2_net_game_data	*FreeSpace2TrackerGameData;

//Start New 7-9-98
static unsigned int LastGameOverPacket;
static unsigned int FirstGameOverPacket;

static int SendingGameOver;
//End New 7-9-98

static void SendClientHolePunch(sockaddr_in6 *addr);

static int SerializeGamePacket(const game_packet_header *gph, ubyte *data)
{
	unsigned int packet_size = 0;

	PXO_ADD_UINT(gph->len);
	PXO_ADD_DATA(gph->game_type);

	//PXO_ADD_SHORT(gph->game_id);	// not used except with fs2open, part of 'junk'
	PXO_ADD_SHORT(Multi_fs_tracker_game_id);	// just set here instead of 15 other places

	PXO_ADD_DATA(gph->junk); // not used, basically just padding for compatibility
	PXO_ADD_INT(gph->type);
	PXO_ADD_UINT(gph->sig);

	switch (gph->type) {
		// these have no other data
		case GNT_CLIENT_ACK:
		case GNT_GAMEOVER:
		case GNT_NAT_HOLE_PUNCH_ACK:
			break;

		// this one may or may not have extra data
		case GNT_GAMELIST_REQ: {
			if (gph->len > GAME_HEADER_ONLY_SIZE) {
				Assert(gph->len == (GAME_HEADER_ONLY_SIZE+sizeof(filter_game_list_struct)));

				auto filter = reinterpret_cast<const filter_game_list_struct *>(&gph->data);

				PXO_ADD_INT(filter->rank);
				PXO_ADD_DATA(filter->channel);
				PXO_ADD_DATA(filter->pad);		// for sizing, so gph->len will match
			}

			break;
		}

		// these two packets are the same except for the type
		// the "STATUS" variant is only to tell PXO that we
		// want to know the connectivity probe status
		case GNT_GAMEUPDATE:
		case GNT_GAMEUPDATE_STATUS: {
			auto game_data = reinterpret_cast<const pxo_net_game_data *>(&gph->data);

			PXO_ADD_DATA(game_data->game_name);
			PXO_ADD_INT(game_data->difficulty);
			PXO_ADD_INT(game_data->type);
			PXO_ADD_INT(game_data->state);
			PXO_ADD_INT(game_data->max_players);
			PXO_ADD_INT(game_data->current_num_players);
			PXO_ADD_DATA(game_data->mission_name);

			PXO_ADD_DATA(game_data->channel);
			PXO_ADD_DATA(game_data->pad);		// for sizing, so gph->len will match

			break;
		}

		case GNT_GAME_COUNT_REQ: {
			Assert(gph->len == (GAME_HEADER_ONLY_SIZE+sizeof(filter_game_list_struct)));

			filter_game_list_struct filter;

			SDL_zero(filter);

			memcpy(filter.channel, gph->data, sizeof(filter.channel));

			PXO_ADD_DATA(filter.channel);

			// add in junk data (ignored on server) to make packet size match
			PXO_ADD_INT(filter.rank);
			PXO_ADD_DATA(filter.pad);

			break;
		}

		// we shouldn't be sending any other packet types
		default:
			Int3();
			break;
	}

	Assert(packet_size >= GAME_HEADER_ONLY_SIZE);
	Assert(packet_size == gph->len);
	Assert(packet_size <= MAX_PACKET_SIZE);

	return static_cast<int>(packet_size);
}

static void DeserializeGamePacket(const ubyte *data, const int data_size, game_packet_header *gph)
{
	int offset = 0;
	int i;

	memset(gph, 0, sizeof(game_packet_header));

	// make sure we received a complete base packet
	if (data_size < static_cast<int>(GAME_HEADER_ONLY_SIZE)) {
		gph->len = 0;
		gph->type = 255;	// invalid = 0xff

		return;
	}

	PXO_GET_UINT(gph->len);
	PXO_GET_DATA(gph->game_type);

	PXO_GET_SHORT(gph->game_id);	// not used except with fs2open, part of 'junk'
	PXO_GET_DATA(gph->junk); // not used, basically just padding for compatibility
	PXO_GET_INT(gph->type);
	PXO_GET_UINT(gph->sig);

	// sanity check data size to make sure we reveived all of the expected packet
	// (the -1 is because psnet2 pops off one byte)
	if (static_cast<int>(gph->len)-1 > data_size) {
		gph->len = 0;
		gph->type = -1;

		return;
	}

	switch (gph->type) {
		case GNT_SERVER_ACK:
			break;

		case GNT_GAMELIST_DATA: {
			auto games = reinterpret_cast<game_list_ip4 *>(&gph->data);

			PXO_GET_DATA(games->game_type);

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_DATA(games->game_name[i]);
			}

			PXO_GET_DATA(games->pad);	// padded bytes for alignment

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_UINT(games->game_server[i]);
			}

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_USHORT(games->port[i]);
			}

			break;
		}

		case GNT_GAMELIST_DATA_NEW: {
			auto games = reinterpret_cast<game_list_ip6 *>(&gph->data);

			PXO_GET_DATA(games->game_type);

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_DATA(games->game_name[i]);
			}

			PXO_GET_DATA(games->pad);	// padded bytes for alignment

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_DATA(games->game_server[i]);
			}

			for (i = 0; i < MAX_GAME_LISTS_PER_PACKET; i++) {
				PXO_GET_USHORT(games->port[i]);
			}

			break;
		}

		case GNT_GAME_COUNT_DATA: {
			int n_users = 0;
			char channel[512];

			PXO_GET_INT(n_users);

			SDL_strlcpy(channel, reinterpret_cast<const char *>(data+offset), SDL_arraysize(channel));
			offset += static_cast<int>(strlen(channel) + 1);

			memcpy(gph->data, &n_users, sizeof(int));
			memcpy(gph->data+sizeof(int), channel, strlen(channel)+1);

			break;
		}

		case GNT_GAME_PROBE_STATUS: {
			int flags = 0;
			int next_try = 0;

			PXO_GET_INT(flags);
			PXO_GET_INT(next_try);

			memcpy(gph->data, &flags, sizeof(int));
			memcpy(gph->data+sizeof(int), &next_try, sizeof(int));

			break;
		}

		case GNT_NAT_HOLE_PUNCH_REQ: {
			// using data_size here since gph.len hasn't been adjusted yet
			if (data_size == (GAME_HEADER_ONLY_SIZE+sizeof(hole_punch_addr_ip6))) {
				auto ipv6 = reinterpret_cast<hole_punch_addr_ip6 *>(&gph->data);

				PXO_GET_DATA(ipv6->addr);
				PXO_GET_USHORT(ipv6->port);
			} else {
				auto ipv4 = reinterpret_cast<hole_punch_addr *>(&gph->data);

				PXO_GET_UINT(ipv4->addr);
				PXO_GET_USHORT(ipv4->port);
			}

			break;
		}

		default:
			break;
	}

	Assert(offset == data_size);
}


int InitGameTrackerClient(int gametype)
{
	GameType = gametype;
	LastTrackerUpdate = 0;
	switch(gametype)
	{
	case GT_FS2OPEN:
		TrackerGameData.len = GAME_HEADER_ONLY_SIZE+sizeof(freespace2_net_game_data);
		break;

	default:
		Int3();
		return 0;
	}
	TrackerGameData.game_type = static_cast<unsigned char>(gametype);	//1==freespace (GT_FREESPACE), 2==D3, 3==tuberacer, etc.
	TrackerGameData.type = GNT_GAMEUPDATE_STATUS;	//Used to specify what to do ie. Add a new net game (GNT_GAMESTARTED), remove a net game (game over), etc.

	FreeSpace2TrackerGameData = reinterpret_cast<freespace2_net_game_data *>(&TrackerGameData.data);
	
	GameListReq.game_type = static_cast<unsigned char>(gametype);
	GameListReq.type = GNT_GAMELIST_REQ;
	GameListReq.len = GAME_HEADER_ONLY_SIZE;

	TrackAckPacket.game_type = static_cast<unsigned char>(gametype);
	TrackAckPacket.len = GAME_HEADER_ONLY_SIZE;
	TrackAckPacket.type = GNT_CLIENT_ACK;

	GameOverPacket.game_type = static_cast<unsigned char>(gametype);
	GameOverPacket.len = GAME_HEADER_ONLY_SIZE;
	GameOverPacket.type = GNT_GAMEOVER;

	// This would be a good place to resolve the IP based on a domain name
	psnet_get_addr(Multi_options_g.game_tracker_ip, GAMEPORT, &gtrackaddr, ADDR_FLAG_PREFER_IPV4);

	SDL_zero(GameBuffer);

	//Start New 7-9-98
	SendingGameOver = 0;
	//End New 7-9-98

	return 1;
}

void IdleGameTracker()
{
	fd_set read_fds;	           
	struct timeval timeout;
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	PSNET_TOP_LAYER_PROCESS();

	timeout.tv_sec=0;            
	timeout.tv_usec=0;
	if((TrackerGameIsRunning) && ((time(nullptr)-LastTrackerUpdate)>TRACKER_UPDATE_INTERVAL) && !SendingGameOver)
	{
		//Time to update the tracker again
		packet_length = SerializeGamePacket(&TrackerGameData, packet_data);
		SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
			   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);

		TrackerAckdUs = 0;
		LastTrackerUpdate = time(nullptr);
	}
	else if((TrackerGameIsRunning)&&(!TrackerAckdUs)&&((timer_get_milliseconds()-LastSentToTracker)>TRACKER_RESEND_TIME))
	{
		//We still haven't been acked by the last packet and it's time to resend.
		packet_length = SerializeGamePacket(&TrackerGameData, packet_data);
		SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
			   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);

		TrackerAckdUs = 0;
		LastTrackerUpdate = time(nullptr);
		LastSentToTracker = timer_get_milliseconds();
	}

	//Start New 7-9-98
	if(SendingGameOver){
		if((timer_get_milliseconds()-LastGameOverPacket)>TRACKER_RESEND_TIME){
			//resend
			packet_length = SerializeGamePacket(&GameOverPacket, packet_data);
			LastGameOverPacket = timer_get_milliseconds();
			SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
				   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);
		} 
		/*
		else if((timer_get_milliseconds()-FirstGameOverPacket)>NET_ACK_TIMEOUT) {
			//Giving up, it timed out.
			SendingGameOver = 2;
		}
		*/
	}
	//End New 7-9-98

	//Check for incoming
	FD_ZERO(&read_fds);	// NOLINT
	FD_SET(Psnet_socket, &read_fds);

	if(SELECT(static_cast<int>(Psnet_socket+1),&read_fds,nullptr,nullptr,&timeout, PSNET_TYPE_GAME_TRACKER))
	{
		int bytesin;
		int addrsize;
		SOCKADDR_STORAGE fromaddr;

		game_packet_header inpacket;

		SDL_zero(inpacket);
		addrsize = sizeof(fromaddr);

		bytesin = RECVFROM(Psnet_socket, reinterpret_cast<char *>(&packet_data), sizeof(game_packet_header), 0,
						   reinterpret_cast<LPSOCKADDR>(&fromaddr), &addrsize, PSNET_TYPE_GAME_TRACKER);

		if (bytesin > 0) {
			DeserializeGamePacket(packet_data, bytesin, &inpacket);

			// subtract one from the header
			inpacket.len--;
#ifndef NDEBUG
		} else {
			int wserr=WSAGetLastError();
			mprintf(("RECVFROM() failure. WSAGetLastError() returned %d\n",wserr));
#endif
		}

		//Check to make sure the packets ok
		if ( (bytesin > 0) && (bytesin == (int)inpacket.len) )
		{
			switch(inpacket.type)
			{
			case GNT_SERVER_ACK:
				//The server got our packet so we can stop sending now
				TrackerAckdUs = 1;				
				
				// 7/13/98 -- because of the FreeSpace iterative frame process -- set this value to 0, instead
				// of to 2 (as it originally was) since we call SendGameOver() only once.  Once we get the ack
				// from the server, we can assume that we are done.
				// need to mark this as 0
				SendingGameOver = 0;							
				break;
			case GNT_GAMELIST_DATA:
			case GNT_GAMELIST_DATA_NEW:
				int i;
				//Woohoo! Game data! put it in the buffer (if one's free)
				for(i=0;i<MAX_GAME_BUFFERS;i++)
				{
					if(GameBuffer[i].game_type==GT_UNUSED)
					{
						if (inpacket.type == GNT_GAMELIST_DATA)
						{
							// convert to ip6 struct
							auto gl4 = reinterpret_cast<game_list_ip4 *>(&inpacket.data);
							game_list_ip6 gl6;
							in_addr addr;

							gl6.game_type = gl4->game_type;
							memcpy(&gl6.game_name, &gl4->game_name, sizeof(gl6.game_name));
							memcpy(&gl6.port, &gl4->port, sizeof(gl6.port));

							for (int j = 0; j < MAX_GAME_LISTS_PER_PACKET; j++)
							{
								addr.s_addr = gl4->game_server[j];
								psnet_map4to6(&addr, &gl6.game_server[j]);
							}

							memcpy(&GameBuffer[i], &gl6, sizeof(game_list));
						}
						else
						{
							memcpy(&GameBuffer[i], &inpacket.data, sizeof(game_list));
						}
						i=MAX_GAME_BUFFERS+1;
					}
				}
				break;

			case GNT_GAME_COUNT_DATA:
				//Here, inpacket.data contains the following structure
				//struct {
				//	int numusers;
				//	char channel[];//Null terminated
				//	}
				//You can add whatever code, or callback, etc. you need to deal with this data

				// let the PXO screen know about this data
				int num_servers;
				char channel[512];

				// get the user count
				memcpy(&num_servers,inpacket.data,sizeof(int));

				// copy the channel name
				SDL_strlcpy(channel, inpacket.data+sizeof(int), SDL_arraysize(channel));

				// send it to the PXO screen				
				multi_pxo_channel_count_update(channel,num_servers);
				break;

			case GNT_GAME_PROBE_STATUS:
				int flags;
				int next_try;

				memcpy(&flags, inpacket.data, sizeof(int));
				memcpy(&next_try, inpacket.data+sizeof(int), sizeof(int));

				// tell user about it
				multi_fs_tracker_report_probe_status(flags, next_try);
				break;

			case GNT_NAT_HOLE_PUNCH_REQ:
				sockaddr_in6 nataddr;

				nataddr.sin6_family = AF_INET6;

				if (inpacket.len == (GAME_HEADER_ONLY_SIZE+sizeof(hole_punch_addr_ip6))) {
					auto ipv6 = reinterpret_cast<hole_punch_addr_ip6 *>(&inpacket.data);

					nataddr.sin6_addr = ipv6->addr;
					nataddr.sin6_port = ipv6->port;
				} else {
					auto ipv4 = reinterpret_cast<hole_punch_addr *>(&inpacket.data);
					in_addr addr;

					addr.s_addr = ipv4->addr;

					psnet_map4to6(&addr, &nataddr.sin6_addr);
					nataddr.sin6_port = ipv4->port;
				}

				SendClientHolePunch(&nataddr);
				break;
			}

			AckPacket(inpacket.sig);			
		}
	}
}

void UpdateGameData(void *buffer)
{
	SendingGameOver = 0;

	switch(GameType){
	case GT_FS2OPEN:
		memcpy(FreeSpace2TrackerGameData,buffer,sizeof(freespace2_net_game_data));
		break;

	default:
		Int3();
		break;
	}

	// forces update
	LastTrackerUpdate = 0;
}

game_list * GetGameList()
{
	static game_list gl;

	for (auto &game : GameBuffer) {
		if (game.game_type != GT_UNUSED) {
			memcpy(&gl, &game, sizeof(game_list));
			game.game_type = GT_UNUSED;
			return &gl;
		}
	}

	return nullptr;
}

void RequestGameList()
{
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	GameListReq.len = GAME_HEADER_ONLY_SIZE;

	packet_length = SerializeGamePacket(&GameListReq, packet_data);
	SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
		   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);
}

void RequestGameListWithFilter(void *filter)
{
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	memcpy(&GameListReq.data,filter,sizeof(filter_game_list_struct));
	GameListReq.len = GAME_HEADER_ONLY_SIZE+sizeof(filter_game_list_struct);

	packet_length = SerializeGamePacket(&GameListReq, packet_data);
	SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
		   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);
}


/* REPLACED BELOW
void SendGameOver()
{
	TrackerGameIsRunning = 0;
	sendto(gamesock,(const char *)&GameOverPacket,GameOverPacket.len,0,(SOCKADDR *)&gtrackaddr,sizeof(SOCKADDR_IN));
}
*/

//Start New 7-9-98
int SendGameOver()
{
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	if(SendingGameOver==2) 
	{
		SendingGameOver = 0;	
		return 1;
	}
	if(SendingGameOver==1) 
	{
		//Wait until it's sent.
		IdleGameTracker();
		return 0;
	}
	if(SendingGameOver==0)
	{
		LastGameOverPacket = timer_get_milliseconds();
		FirstGameOverPacket = timer_get_milliseconds();
		SendingGameOver = 1;
		TrackerGameIsRunning = 0;

		packet_length = SerializeGamePacket(&GameOverPacket, packet_data);
		SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
			   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);

		return 0;
	}
	return 0;
}
//End New 7-9-98

void AckPacket(int sig)
{
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	TrackAckPacket.sig = sig;

	packet_length = SerializeGamePacket(&TrackAckPacket, packet_data);
	SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
		   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);
}

void StartTrackerGame(void *buffer)
{
	SendingGameOver = 0;

	switch(GameType){
	case GT_FS2OPEN:
		memcpy(FreeSpace2TrackerGameData,buffer,sizeof(freespace2_net_game_data));
		break;

	default:
		Int3();
		break;
	}
	TrackerGameIsRunning = 1;
	LastTrackerUpdate = 0;	
}

//A new function
void RequestGameCountWithFilter(void *filter) 
{
	game_packet_header GameCountReq{};
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	GameCountReq.game_type = GT_FS2OPEN;
	GameCountReq.type = GNT_GAME_COUNT_REQ;
	GameCountReq.len = GAME_HEADER_ONLY_SIZE+sizeof(filter_game_list_struct);
	memcpy(&GameCountReq.data, reinterpret_cast<const filter_game_list_struct *>(filter)->channel, CHANNEL_LEN);

	packet_length = SerializeGamePacket(&GameCountReq, packet_data);
	SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
		   reinterpret_cast<LPSOCKADDR>(&gtrackaddr), sizeof(gtrackaddr), PSNET_TYPE_GAME_TRACKER);
}

static void SendClientHolePunch(sockaddr_in6 *addr)
{
	game_packet_header HolePunchAck;
	ubyte packet_data[sizeof(game_packet_header)];
	int packet_length = 0;

	// if client is IPv6, and we can't do that, then just skip it
	if ( !IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && !(psnet_get_ip_mode() & PSNET_IP_MODE_V6) ) {
		return;
	}

	HolePunchAck.game_type = GT_FS2OPEN;
	HolePunchAck.type = GNT_NAT_HOLE_PUNCH_ACK;
	HolePunchAck.len = GAME_HEADER_ONLY_SIZE;
	HolePunchAck.sig = 0; // to make sure tracker ignores this packet when it's ACK'd there

	packet_length = SerializeGamePacket(&HolePunchAck, packet_data);
	SENDTO(Psnet_socket, reinterpret_cast<char *>(&packet_data), packet_length, 0,
			reinterpret_cast<LPSOCKADDR>(addr), sizeof(sockaddr_in6), PSNET_TYPE_GAME_TRACKER);
}
