/*
    Pinger: a console based game server browser for Linux

    Copyright (C) 1999 2000 jd [next@captured.com]

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Available: http://www.captured.com/modshop/
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <math.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>

#define LINE_SIZE 100
#define MAX_SIM 5
#define PACKET_SIZE 1000
#define SORT_SIZE 100
#define DISPLAY_SIZE 20
#define RECENT_SIZE 9
#define MIN_PLAYERS 5
#define OPEN_PLAYERS 1
#define MASTER_PACKET_SIZE 5000

#define SERVERLIST_DOWNLOAD 0
#define SERVERLIST_LAST 1
#define SERVERLIST_RECENT 2

struct serverInfo{
	char address[LINE_SIZE];
	char name[LINE_SIZE];
	char mod[LINE_SIZE];
	char map[LINE_SIZE];
	int maxPlayers;
	int players;
	int ping;
	struct serverInfo *next;
} *first, *selected, *sorted[SORT_SIZE];

int advancedMode = 0;

int minPlayers = MIN_PLAYERS;
int openPlayers = OPEN_PLAYERS;
int serverList = SERVERLIST_DOWNLOAD;
char serverQuery[LINE_SIZE];

char game[LINE_SIZE];
char mod[LINE_SIZE];

char listServer[LINE_SIZE];
char listURL[LINE_SIZE];
char gameDirectory[LINE_SIZE];
char gameCmd[LINE_SIZE];

int serverCount;

void trimString(char *line){
	int i;
	int max;

	max = strlen(line);
	for(i = 0; i < max; i++){
		if(line[i] == 13 || line[i] == 10){
			line[i] = 0;
			break;
		}
	}

	for(i = max - 1; line[i] == 32 && i >= 0; i--)
		line[i] = 0;
}

void selectGame(){
	char c[LINE_SIZE];
	int i;

	printf("Select game:\n");

	//game specific
	printf("  1. QuakeWorld\n");
	printf("  2. Quake II\n");

	while(1){
		printf(">");
		fgets(c, LINE_SIZE, stdin);
		trimString(c);
		i = atoi(c);
		if(i == 1){
			strcpy(game, "quakeworld");
			break;
		}else if(i == 2){
			strcpy(game, "quake2");
			break;
		}
	}
	printf("\n");
}

void getGameCfg(){
	FILE *file;
	char *c;

	c = (char *) malloc(strlen(game) + 10);
	strcpy(c, game);
	strcat(c, ".cfg");

	file = fopen(c, "r");

	free(c);

	fgets(listServer, LINE_SIZE, file);
	trimString(listServer);
	fgets(listURL, LINE_SIZE, file);
	trimString(listURL);
	fgets(gameDirectory, LINE_SIZE, file);
	trimString(gameDirectory);
	fgets(gameCmd, LINE_SIZE, file);
	trimString(gameCmd);

	fclose(file);
}

void selectMod(){
	printf("Enter mod directory:\n");
	printf(">");
	fgets(mod, LINE_SIZE, stdin);
	trimString(mod);
	printf("\n");
}

void selectMinPlayers(){
	char c[LINE_SIZE];

	printf("Enter minimum amount of players:\n");
	printf(">");
	fgets(c, LINE_SIZE, stdin);
	trimString(c);
	if(strcmp(c, ""))
		minPlayers = atoi(c);
	printf("\n");
}

void selectOpenPlayers(){
	char c[LINE_SIZE];
	int i;

	printf("Enter minimum amount of open player slots:\n");
	printf(">");
	fgets(c, LINE_SIZE, stdin);
	trimString(c);
	if(strcmp(c, ""))
		openPlayers = atoi(c);
	printf("\n");
}

void selectServerQuery(){
	printf("Enter server name query:\n");
	printf(">");
	fgets(serverQuery, LINE_SIZE, stdin);
	trimString(serverQuery);
	printf("\n");
}

void selectServerList(){
	char c[LINE_SIZE];
	int i;

	printf("Select server list:\n");

	printf("  1. Download new list\n");
	printf("  2. Use last list\n");
	printf("  3. Use recent list\n");

	while(1){
		printf(">");
		fgets(c, LINE_SIZE, stdin);
		trimString(c);
		i = atoi(c);
		if(i == 0 || i == 1){
			serverList = SERVERLIST_DOWNLOAD;
			break;
		}else if(i == 2){
			serverList = SERVERLIST_LAST;
			break;
		}else if(i == 3){
			serverList = SERVERLIST_RECENT;
			break;
		}
	}
	printf("\n");
}

void parseAddress(char *address, char *out){
	int i;

	for(i = 0; address[i] != 0 && address[i] != ':'; i++)
		out[i] = address[i];
	out[i] = 0;
}

int parseMasterPort(char *address){
	int length;
	int i;
	char *port = NULL;

	length = strlen(address);
	for(i = 0; i < length; i++){
		if(address[i] == ':'){
			port = &address[i + 1];
			break;
		}
	}
	if(port == NULL){
		//game specific
		if(!strcmp(game, "quake2"))
			return 27900;
		else if(!strcmp(game, "quakeworld"))
			return 27000;
	}
	return (short) atoi(port);
}

void getMasterMessage(char *out){
	//game specific
	if(!strcmp(game, "quake2")){
		out[0] = 'q';
		out[1] = 'u';
		out[2] = 'e';
		out[3] = 'r';
		out[4] = 'y';
		out[5] = 10;
		out[6] = 0;
	}else if(!strcmp(game, "quakeworld")){
		out[0] = 'c';
		out[1] = 10;
		out[2] = 0;	
	}
}

void downloadFromMaster(){
	int sock;
	struct sockaddr_in *localAddress;
	char message[LINE_SIZE];
	struct sockaddr_in *remoteAddress;
	char c[LINE_SIZE];
	struct sockaddr_in *receivedAddress;
	int size;
	int length;
	char received[MASTER_PACKET_SIZE];
	struct in_addr address;
	short port;
	struct serverInfo *current;
	int i;

	sock = socket(AF_INET, SOCK_DGRAM, 0);

	localAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	localAddress->sin_family = AF_INET;
	localAddress->sin_port = 0;
	localAddress->sin_addr.s_addr = INADDR_ANY;
	bind(sock, localAddress, sizeof(struct sockaddr_in));
	free(localAddress);

	getMasterMessage(message);

	remoteAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	remoteAddress->sin_family = AF_INET;
	remoteAddress->sin_port = htons(parseMasterPort(listURL));
	parseAddress(listURL, c);
	remoteAddress->sin_addr.s_addr = inet_addr(c);
	sendto(sock, message, strlen(message) + 1, 0, remoteAddress, sizeof(struct sockaddr_in));
	free(remoteAddress);

	receivedAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	size = sizeof(struct sockaddr_in);
	length = recvfrom(sock, received, MASTER_PACKET_SIZE - 1, 0, receivedAddress, &size);
	free(receivedAddress);

	close(sock);

	memcpy(&address.s_addr, received + 6, 4);
	memcpy(&port, received + 10, 2);
	first = (struct serverInfo *) malloc(sizeof(struct serverInfo));
	sprintf(first->address, "%s:%d", inet_ntoa(address), htons(port));

	serverCount = 1;
	current = first;
	for(i = 12; i < length; i += 6){
		memcpy(&address.s_addr, received + i, 4);
		memcpy(&port, received + i + 4, 2);
		current->next = (struct serverInfo *) malloc(sizeof(struct serverInfo));
		current = current->next;
		sprintf(current->address, "%s:%d", inet_ntoa(address), htons(port));
		serverCount++;
	}

	current->next = NULL;
}

void downloadServerList(){
	int sock;
	struct sockaddr_in *localAddress;
	int size;
	struct hostent *listHost;
	struct sockaddr_in *remoteAddress;
	int i;
	char *getCmd;
	FILE *sockStream;
	char buffer[LINE_SIZE];
	struct serverInfo *current;
	char *c;

	sock = socket(AF_INET, SOCK_STREAM, 0);

	localAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	localAddress->sin_family = AF_INET;
	localAddress->sin_port = 0;
	localAddress->sin_addr.s_addr = INADDR_ANY;
	bind(sock, localAddress, sizeof(struct sockaddr_in));
	free(localAddress);

	listHost = gethostbyname(listServer);

	remoteAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	remoteAddress->sin_family = AF_INET;
	remoteAddress->sin_port = htons(80);
	remoteAddress->sin_addr = *((struct in_addr *) listHost->h_addr);

	i = connect(sock, remoteAddress, sizeof(struct sockaddr_in));
	if(i == -1){
		printf("ERROR: Unable to connect to list server.\n");
		exit(1);
	}

	free(remoteAddress);

	getCmd = (char *) malloc(strlen(listURL) + 20);
	strcpy(getCmd, "GET ");
	strcat(getCmd, listURL);
	strcat(getCmd, " HTTP/1.0\n\n");
	send(sock, getCmd, strlen(getCmd) + 1, 0);
	free(getCmd);

	sockStream = fdopen(sock, "r");

	while(buffer[0] != 10 && buffer[0] != 13)
		fgets(buffer, LINE_SIZE, sockStream);

	first = (struct serverInfo *) malloc(sizeof(struct serverInfo));
	fgets(buffer, LINE_SIZE, sockStream);
	trimString(buffer);
	strcpy(first->address, buffer);
	current = first;
	serverCount = 1;
	while(1){
		c = fgets(buffer, LINE_SIZE, sockStream);
		if(c == NULL || buffer[0] == 'X')
			break;
		trimString(buffer);
		current->next = (struct serverInfo *) malloc(sizeof(struct serverInfo));
		current = current->next;
		strcpy(current->address, buffer);
		serverCount++;
	}
	current->next = NULL;

	fclose(sockStream);
	close(sock);
}

void useServerList(char *list){
	FILE *file;
	char *c;
	struct serverInfo *current;
	char buffer[LINE_SIZE];

	c = (char *) malloc(strlen(game) + strlen(list) + 1);
	strcpy(c, game);
	strcat(c, list);

	file = fopen(c, "r");

	free(c);

	fgets(buffer, LINE_SIZE, file);
	first = (struct serverInfo *) malloc(sizeof(struct serverInfo));
	strncpy(first->address, buffer + 26, 21);
	trimString(first->address);
	serverCount = 1;

	current = first;

	while(fgets(buffer, LINE_SIZE, file) != NULL){
		current->next = (struct serverInfo *) malloc(sizeof(struct serverInfo));
		current = current->next;
		strncpy(current->address, buffer + 26, 21);
		trimString(current->address);
		serverCount++;
	}

	current->next = NULL;

	fclose(file);
}

int getTime(){
	struct timeval *time;
	int milli;

	time = (struct timeval *) malloc(sizeof(struct timeval));
	gettimeofday(time, NULL);
	milli = time->tv_usec / 1000;
	free(time);
	return milli;
}

short parsePort(char *address){
	int length;
	int i;
	char *port = NULL;

	length = strlen(address);
	for(i = 0; i < length; i++){
		if(address[i] == ':'){
			port = &address[i + 1];
			break;
		}
	}
	if(port == NULL){
		//game specific
		if(!strcmp(game, "quake2"))
			return 27910;
		else if(!strcmp(game, "quakeworld"))
			return 27500;
	}
	return (short) atoi(port);
}

void getMessage(char *out){
	//game specific
	if(!strcmp(game, "quake2") || !strcmp(game, "quakeworld")){
		out[0] = 255;
		out[1] = 255;
		out[2] = 255;
		out[3] = 255;
		out[4] = 's';
		out[5] = 't';
		out[6] = 'a';
		out[7] = 't';
		out[8] = 'u';
		out[9] = 's';
		out[10] = 10;
		out[11] = 0;
	}
}

void parseVar(char *data, char *var, char *def, char *out){
	//game specific
	char search[LINE_SIZE];
	char *c;
	int i;

	sprintf(search, "\\%s\\", var);
	c = strstr(data, search);
	if(c == NULL){
		strcpy(out, def);
		return;
	}
	c += strlen(search);
	for(i = 0 ; c[i] != '\\' && c[i] != '\n'; i++)
		out[i] = c[i];
	out[i] = 0;
}

int playerCount(char *data){
	//game specific
	int count = -2;
	int i;

	for(i = 0; data[i] != 0; i++){
		if(data[i] == '\n')
			count++;
	}

	return count;
}

void pingServers(){
	struct serverInfo *current;
	int sock;
	struct sockaddr_in *localAddress;
	int i;
	struct serverInfo *sent[MAX_SIM];
	char message[LINE_SIZE];
	int done = 0;
	struct sockaddr_in *remoteAddress;
	char c[LINE_SIZE];
	char received[PACKET_SIZE];
	struct sockaddr_in *receivedAddress;
	int size;
	int time;
	int completed = 0;
	int percentage;
	int nextMark = 2;

	printf("Pinging %d servers...\n\n", serverCount);
	printf("0                                               100\n");
	putchar('|');
	fflush(stdout);

	current = first;

	sock = socket(AF_INET, SOCK_DGRAM, 0);

	localAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
	localAddress->sin_family = AF_INET;
	localAddress->sin_port = 0;
	localAddress->sin_addr.s_addr = INADDR_ANY;
	bind(sock, localAddress, sizeof(struct sockaddr_in));
	free(localAddress);

	fcntl(sock, F_SETFL, O_NONBLOCK);

	for(i = 0; i < MAX_SIM; i++)
		sent[i] = NULL;

	getMessage(message);

	while(!done){
		//check for openings in 'sent' and ping/add servers
		for(i = 0; i < MAX_SIM; i++){
			if(sent[i] == NULL && current != NULL){
				remoteAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
				remoteAddress->sin_family = AF_INET;
				remoteAddress->sin_port = htons(parsePort(current->address));
				parseAddress(current->address, c);
				remoteAddress->sin_addr.s_addr = inet_addr(c);
				sendto(sock, message, strlen(message) + 1, 0, remoteAddress, sizeof(struct sockaddr_in));
				free(remoteAddress);
				current->ping = getTime();
				sent[i] = current;
				current = current->next;
				completed++;
			}
		}

		//recieve packets, record data and remove recieved servers from 'sent'
		receivedAddress = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
		size = sizeof(struct sockaddr_in);
		i = recvfrom(sock, received, PACKET_SIZE - 1, 0, receivedAddress, &size);
		if(i > 0){
			received[i] = 0;
			sprintf(c, "%s:%d", inet_ntoa(receivedAddress->sin_addr), ntohs(receivedAddress->sin_port));
			for(i = 0; i < MAX_SIM; i++){
				if(sent[i] != NULL){
					if(!strcmp(c, sent[i]->address)){
						time = getTime();
						if(time < sent[i]->ping)
							time += 1000;
						sent[i]->ping = time - sent[i]->ping;
						//game specific
						if(!strcmp(game, "quake2")){
							parseVar(received, "game", "baseq2", sent[i]->mod);
							parseVar(received, "mapname", "somewhere", sent[i]->map);
						}else if(!strcmp(game, "quakeworld")){
							parseVar(received, "*gamedir", "id1", sent[i]->mod);							
							parseVar(received, "map", "somewhere", sent[i]->map);
						}
						parseVar(received, "hostname", "noname", sent[i]->name);
						parseVar(received, "maxclients", "0", c);
						sent[i]->maxPlayers = atoi(c);
						sent[i]->players = playerCount(received);
						sent[i] = NULL;
					}
				}
			}
		}
		free(receivedAddress);

		//remove timed out servers from 'sent'
		for(i = 0; i < MAX_SIM; i++){
			if(sent[i] != NULL){
				time = getTime();
				if(time < sent[i]->ping)
					time += 1000;
				if((time - sent[i]->ping) >= 999){
					strcpy(sent[i]->name, "???");
					strcpy(sent[i]->mod, "???");
					strcpy(sent[i]->map, "???");
					sent[i]->maxPlayers = 0;
					sent[i]->players = 0;
					sent[i]->ping = 999;
					sent[i] = NULL;
				}
			}
		}

		//check for completion
		if(current == NULL){
			done = 1;
			for(i = 0; i < MAX_SIM; i++){
				if(sent[i] != NULL)
					done = 0;
			}
		}

		//output status
		percentage = (int) floor(((double) completed / (double) serverCount) * 100);
		if(percentage > nextMark){
			putchar('|');
			fflush(stdout);
			nextMark += 2;
		}
	}
	printf("\n\n");
	close(sock);
}

void lowercase(char *line, char *out){
	int i;

	for(i = 0; i < LINE_SIZE; i++)
		out[i] = tolower(line[i]);
}

void sortServers(){
	int i;
	struct serverInfo *current;
	char name[LINE_SIZE];
	int j;

	for(i = 0; i < SORT_SIZE; i++)
		sorted[i] = NULL;

	lowercase(serverQuery, serverQuery);

	for(current = first; current != NULL; current = current->next){
		lowercase(current->name, name);
		if(
			(!strcasecmp(current->mod, mod) || !strcmp(mod, "")) &&
			current->players <= current->maxPlayers - openPlayers &&
			current->players >= minPlayers &&
			(strstr(name, serverQuery) != NULL || !strcmp(serverQuery, ""))
		){
			for(i = 0; i < SORT_SIZE; i++){
				if(sorted[i] == NULL){
					sorted[i] = current;
					break;
				}else if(current->ping <= sorted[i]->ping){
					for(j = SORT_SIZE - 1; j > i; j--)
						sorted[j] = sorted[j - 1];
					sorted[i] = current;
					break;
				}
			}
		}
	}
}

void printServer(FILE *file, struct serverInfo *server){
	char name[LINE_SIZE];
	char address[LINE_SIZE];
	char mod[LINE_SIZE];
	char map[LINE_SIZE];

	strcpy(name, server->name);
	strcpy(address, server->address);
	strcpy(mod, server->mod);
	strcpy(map, server->map);
	name[25] = 0;
	address[22] = 0;
	mod[8] = 0;
	map[8] = 0;

	fprintf(file, "%-25s %-22s %-3d %-8s %-8s %-2d/%-2d\n",
		name,
		address,
		server->ping,
		mod,
		map,
		server->players,
		server->maxPlayers
	);
}

void outputServers(){
	char *c;
	FILE *file;
	int i;

	c = (char *) malloc(strlen(game) + 10);
	strcpy(c, game);
	strcat(c, ".list");

	file = fopen(c, "w");
	for(i = 0; i < SORT_SIZE; i++){
		if(sorted[i] != NULL)
			printServer(file, sorted[i]);
	}
	fclose(file);
	free(c);
}

void beep(){
	int fd;
	int i;
	int j;
	char c = 255;
	char d = 0;

	fd = open("/dev/dsp", O_WRONLY);
	for(i = 0; i < 100; i++){
		for(j = 0; j < 10; j++)
			write(fd, &d, sizeof(d));
		write(fd, &c, sizeof(c));
	}
	close(fd);
}

void selectServer(){
	char selection[LINE_SIZE];
	int i;

	for(i = 0; i < DISPLAY_SIZE; i++){
		if(sorted[i] != NULL){
			printf("%-2d ", i + 1);
			printServer(stdout, sorted[i]);
		}
	}
	beep();
	printf("\n");
	while(1){
		printf(">");
		fgets(selection, LINE_SIZE, stdin);
		trimString(selection);
		i = atoi(selection);
		if(i >= 1 && i <= DISPLAY_SIZE && sorted[i - 1] != NULL){
			selected = sorted[i - 1];
			break;
		}
	}
	printf("\n");
}

void startGame(){
	char address[LINE_SIZE];
	char mod[LINE_SIZE];
	char *c;
	FILE *file;
	int i;
	char recent[RECENT_SIZE][LINE_SIZE];
	char inAddress[LINE_SIZE];
	struct serverInfo *current;
	struct serverInfo *old;
	char command[LINE_SIZE];
	char commanda[LINE_SIZE];

	strcpy(address, selected->address);
	strcpy(mod, selected->mod);

	c = (char *) malloc(strlen(game) + 10);
	strcpy(c, game);
	strcat(c, ".recent");

	for(i = 0; i < RECENT_SIZE; i++)
		recent[i][0] = 0;

	file = fopen(c, "r");
	if(file != NULL){
		for(i = 0; i < RECENT_SIZE; i++){
			fgets(recent[i], LINE_SIZE, file);
			strcpy(inAddress, recent[i] + 26);
			inAddress[21] = 0;
			trimString(inAddress);
			if(!strcmp(address, inAddress))
				i--;
		}
		fclose(file);
	}

	file = fopen(c, "w");
	printServer(file, selected);
	for(i = 0; i < RECENT_SIZE; i++)
		fprintf(file, "%s", recent[i]);
	fclose(file);

	free(c);

	current = first;
	while(current != NULL){
		old = current;
		current = current->next;
		free(old);
	}

	sprintf(command, "cd %s\n", gameDirectory);

	//game specific
	if(!strcmp(game, "quake2"))
		sprintf(commanda, "%s +set game %s +connect %s\n", gameCmd, mod, address);
	else if(!strcmp(game, "quakeworld"))
		sprintf(commanda, "%s +connect %s\n", gameCmd, address);

	strcat(command, commanda);
	system(command);
}

int main(int argc, char **args){
	printf("\n");
	printf("--\n");
	printf("Pinger v2.0b\n");
	printf("http://www.captured.com/modshop/\n");
	printf("--\n");
	printf("\n");

	strcpy(serverQuery, "");

	if(argc > 1 && !strcmp(args[1], "a"))
		advancedMode = 1;

	selectGame();
	getGameCfg();
	selectMod();
	if(advancedMode){
		selectMinPlayers();
		selectOpenPlayers();
		selectServerQuery();
		selectServerList();
	}
	if(serverList == SERVERLIST_DOWNLOAD){
		if(!strcmp(listServer, "master"))
			downloadFromMaster();
		else
			downloadServerList();
	}else if(serverList == SERVERLIST_LAST)
		useServerList(".list");
	else if(serverList == SERVERLIST_RECENT)
		useServerList(".recent");
	pingServers();
	sortServers();
	if(serverList == SERVERLIST_DOWNLOAD)
		outputServers();
	selectServer();
	startGame();

	return 0;
}
