#include "quakedef.h"

#ifdef RGLQUAKE
#include "glquake.h"	//overkill
#endif

#ifndef _WIN32
#include <unistd.h>
#endif

#define         ENV_READ_NAME   "FTE_SECURE_CHANNEL_READ"
#define         ENV_WRITE_NAME  "FTE_SECURE_CHANNEL_WRITE"

#define SECURE_CMD_CHECKMODEL   'c' // q: name:string, model:buffer
                                    // a: 'y' or 'n'

#define SECURE_CMD_GETVERSION   'g' // q: check_line:string, serverinfo:string, userinfo:string
                                    // a: ok, client_desc:string, crc:ulong

#define SECURE_CMD_CHECKVERSION 'v' // q: check_line:string, serverinfo:string, userinfo:string
                                    // a: ok, crc:ulong
#define SECURE_CMD_CHECKVERSION2	'r'	//SECURE_CMD_CHECKVERSION with the engine description appended on the end.
										//let's my front end work on a variety of engines rathar than just 1

#define SECURE_ANSWER_OK        'y'
#define SECURE_ANSWER_YES       'y'
#define SECURE_ANSWER_NO        'n'
#define SECURE_ANSWER_ERROR     'n'

cvar_t allow_f_version		= {"allow_f_version", "1"};
cvar_t allow_f_server		= {"allow_f_server", "1"};
cvar_t allow_f_modified		= {"allow_f_modified", "1"};
cvar_t allow_f_skins		= {"allow_f_skins", "1"};
cvar_t auth_validateclients	= {"auth_validateclients", "1"};

//
// last f_queries
//
typedef struct f_query_s
{
    char *query;
    char *serverinfo;
    char *c_userinfo[MAX_CLIENTS];
    qboolean c_exist[MAX_CLIENTS];

    unsigned short crc;
    double timestamp;
}
f_query_t;

#define F_QUERIES_REMEMBERED 5
f_query_t f_last_queries[F_QUERIES_REMEMBERED];
int f_last_query_pos = 0;

typedef struct f_modified_s {
	char name[MAX_QPATH];
	qboolean ismodified;
	struct f_modified_s *next;
} f_modified_t;

f_modified_t *f_modified_list;
qboolean care_f_modified;
qboolean f_modified_particles;

#ifdef _WIN32
#include "winquake.h"

typedef HANDLE qpipe;

// write to pipe, returns number of bytes written, or 0 = error
int Sys_WritePipe(qpipe q_pipe, unsigned char *buf, int buflen)
{
    DWORD   dwBytesWritten;
    BOOL    ret;

    ret = WriteFile(
        q_pipe,
        (LPVOID) buf,
        buflen,
        &dwBytesWritten,
        NULL);

    if (ret)
        return dwBytesWritten;
    else
        return 0;
}

// read from pipe, returns number of bytes read, or 0 = error
int Sys_ReadPipe(qpipe q_pipe, unsigned char *buf, int buflen)
{
    DWORD   dwBytesRead;
    BOOL    ret;

    ret = ReadFile(
        q_pipe,
        (LPVOID) buf,
        buflen,
        &dwBytesRead,
        NULL);

    if (ret)
        return dwBytesRead;
    else
        return 0;
}
#else
typedef int qpipe;

int Sys_WritePipe(qpipe q_pipe, unsigned char *buf, int buflen)
{
	return write(q_pipe, buf, buflen);
}

int Sys_ReadPipe(qpipe q_pipe, unsigned char *buf, int buflen)
{
	return read(q_pipe, buf, buflen);
}
#endif

static qpipe f_read, f_write;

int  SPipe_ReadMemory(qpipe read_pipe, unsigned char *buf, int buflen)
{
    int completed = 0;

    while (completed < buflen)
    {
        int read;

        read = Sys_ReadPipe(read_pipe, buf+completed, buflen-completed);

        if (read == 0)
            return false;

        completed += read;
    }
    return true;
}

int SPipe_ReadChar(qpipe read_pipe, char *c)
{
    return SPipe_ReadMemory(read_pipe, (unsigned char *)c, 1);
}

int SPipe_ReadInt(qpipe read_pipe, int *val)
{
    return SPipe_ReadMemory(read_pipe, (unsigned char *)val, sizeof(int));
}

int SPipe_ReadUlong(qpipe read_pipe, unsigned long *val)
{
    return SPipe_ReadMemory(read_pipe, (unsigned char *)val, sizeof(unsigned long));
}

int SPipe_ReadString(qpipe read_pipe, char *buf, int buflen)
{
	int i;
	int slen;
	if (!SPipe_ReadInt(read_pipe, &slen))
		return false;

	for (i = 0; i < buflen && i < slen; i++)
	{
		if (!SPipe_ReadChar(read_pipe, buf+i))
			return false;
	}
	buf[i] = '\0';
	for (; i < slen; i++)	//now read the extra data that wouldn't fit.
	{
		if (!SPipe_ReadChar(read_pipe, buf+i))
			return false;
	}

	return true;
}

int  SPipe_WriteMemory(qpipe write_pipe, unsigned char *buf, int buflen)
{
    int completed = 0;

    while (completed < buflen)
    {
        int written;

        written = Sys_WritePipe(write_pipe, buf+completed, buflen-completed);

        if (written == 0)
            return written;

        completed += written;
    }
    return completed;
}

int SPipe_WriteChar(qpipe write_pipe, char c)
{
    int written;
    written = SPipe_WriteMemory(write_pipe, (unsigned char *)(&c), 1);

    return (written==1);
}

int SPipe_WriteInt(qpipe write_pipe, int val)
{
    int written;
    written = SPipe_WriteMemory(write_pipe, (unsigned char *)(&val), sizeof(int));

    return (written==sizeof(int));
}

int SPipe_WriteString(qpipe write_pipe, char *string)
{
	int i;
	int len = strlen(string);
	if (!SPipe_WriteInt(write_pipe, len))
		return false;

	for (i = 0; i < len; i++)
		if (!SPipe_WriteMemory(write_pipe, (unsigned char *)(string+i), 1))
			return false;

	return true;
}

void CRC_AddBlock (unsigned short *crcvalue, qbyte *start, int count)
{
    while (count--)
		CRC_ProcessByte(crcvalue, *start++);
}

unsigned short SCRC_GetQueryStateCrc(char *f_query_string)
{
    unsigned short crc;
    int i;
    char *tmp;

    CRC_Init(&crc);

    // add query
    CRC_AddBlock(&crc, f_query_string, strlen(f_query_string));

    // add snapshot of serverinfo
    tmp = Info_ValueForKey(cl.serverinfo, "deathmatch");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "teamplay");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "hostname");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "*progs");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "map");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "spawn");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "watervis");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "fraglimit");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "*gamedir");
    CRC_AddBlock(&crc, tmp, strlen(tmp));
    tmp = Info_ValueForKey(cl.serverinfo, "timelimit");
    CRC_AddBlock(&crc, tmp, strlen(tmp));

    // add snapshot of userinfo for every connected client
    for (i=0; i < MAX_CLIENTS; i++)
        if (cl.players[i].name[0])
        {
            tmp = Info_ValueForKey(cl.players[i].userinfo, "name");
            CRC_AddBlock(&crc, tmp, strlen(tmp));
            tmp = Info_ValueForKey(cl.players[i].userinfo, "team");
            CRC_AddBlock(&crc, tmp, strlen(tmp));
        }

    // done
    return crc;
}

void InitValidation(void)
{
	char *read, *write;
	read = getenv(ENV_READ_NAME);
	write = getenv(ENV_WRITE_NAME);

	Cvar_Register(&allow_f_version,	"Authentication");
	Cvar_Register(&allow_f_modified,	"Authentication");
	Cvar_Register(&allow_f_skins,	"Authentication");
	Cvar_Register(&auth_validateclients,	"Authentication");

	if (!read || !write)
		return;

	f_read = (qpipe)atoi(read);
	f_write = (qpipe)atoi(write);

	if (!f_read || !f_write)
	{
		f_write = f_read = 0;
		return;
	}

}

void ValidationThink (void)
{
}

void ValidationSendRequest (void)
{
}


void  Validation_FilesModified (void)
{
	f_modified_t *fm;
    int count=0;
    char buf[1024];
    buf[0] = 0;

	if (!allow_f_modified.value)
		return;

    care_f_modified = true;

	if (f_modified_particles)
	{
		strcat(buf, "modified: particles");
		count++;
	}

    for (fm = f_modified_list; fm; fm = fm->next)
	{
        if (fm->ismodified)
        {
            char *tmp;
            if (!count)
                strcat(buf, "modified:");
            if (strlen(buf) < 250)
            {
                tmp = fm->name+1;
                while (strchr(tmp, '/'))
                    tmp = strchr(tmp, '/')+1;
                strcat(buf, " ");
                strcat(buf, tmp);
                count++;
            }
            else
            {
                strcat(buf, " & more...");
                break;
            }
        }
	}
    if (count == 0)
        strcat(buf, "all models ok");
    
    Cbuf_AddText("say ", RESTRICT_LOCAL);
    Cbuf_AddText(buf, RESTRICT_LOCAL);
    Cbuf_AddText("\n", RESTRICT_LOCAL);
}

void Validation_IncludeFile(char *filename, char *file, int filelen)
{
	char result;
	f_modified_t *fm;

	for (fm = f_modified_list; fm; fm = fm->next)
	{
		if (!strcmp(fm->name, filename))
			break;
	}
	if (!fm)
	{
		fm = Z_Malloc(sizeof(f_modified_t));
		fm->next = f_modified_list;
		f_modified_list = fm;
		Q_strncpyz(fm->name, filename, sizeof(fm->name));
	}

	fm->ismodified = true;
	if (f_read && allow_f_modified.value)
	{
		SPipe_WriteChar(f_write, SECURE_CMD_CHECKMODEL);
		SPipe_WriteString(f_write, fm->name);
		SPipe_WriteInt (f_write, filelen);
		SPipe_WriteMemory(f_write, file, filelen);

		SPipe_ReadChar(f_read, &result);

		if (result == SECURE_ANSWER_YES)
			fm->ismodified = false;
	}
	if (fm->ismodified && care_f_modified)
	{
		Cbuf_AddText("say previous f_modified response is no longer valid.\n", RESTRICT_LOCAL);
		care_f_modified = false;
	}
}

void Validation_FlushFileList(void)
{
	f_modified_t *fm;
	while(f_modified_list)
	{
		fm = f_modified_list->next;

		Z_Free(f_modified_list);
		f_modified_list = fm;
	}
}

void ValidationPrintVersion(char *f_query_string)
{
	f_query_t *this_query;
	unsigned short query_crc;
	unsigned long crc;
	char answer;
	char name[128];
	char sr[256];
	int i;

	switch(qrenderer)
	{
#ifdef RGLQUAKE
	case QR_OPENGL:
		*sr = *"";
		break;
#endif
#ifdef SWQUAKE
	case QR_SOFTWARE:
		strcpy(sr, (r_pixbytes==4?"32bpp":"8bpp"));
		break;
#endif
	default:
		*sr = *"";
		break;
	}
	if (f_read && allow_f_version.value)
	{
		query_crc = SCRC_GetQueryStateCrc(f_query_string);

		//
		// remember this f_version
		//
		this_query = &f_last_queries[f_last_query_pos++ % F_QUERIES_REMEMBERED];
		this_query->timestamp = realtime;
		this_query->crc = query_crc;
		if (this_query->query)
			BZ_Free(this_query->query);
		this_query->query = BZ_Malloc(strlen(f_query_string)+1);
		strcpy(this_query->query, f_query_string);
		if (this_query->serverinfo)
			BZ_Free(this_query->serverinfo);
		this_query->serverinfo = BZ_Malloc(strlen(cl.serverinfo)+1);
		strcpy(this_query->serverinfo, cl.serverinfo);
		for (i=0; i < MAX_CLIENTS; i++)
		{
			if (this_query->c_userinfo[i])
			{
				BZ_Free(this_query->c_userinfo[i]);
				this_query->c_userinfo[i] = NULL;
			}
			this_query->c_exist[i] = false;

			if (cl.players[i].name[0])
			{
				this_query->c_exist[i] = true;
				this_query->c_userinfo[i] = BZ_Malloc(strlen(cl.players[i].userinfo)+1);
				strcpy(this_query->c_userinfo[i], cl.players[i].userinfo);
			}
		}

		//now send the data.

		SPipe_WriteChar(f_write, SECURE_CMD_GETVERSION);
        SPipe_WriteString(f_write, f_query_string);
        SPipe_WriteString(f_write, cl.serverinfo);
        SPipe_WriteString(f_write, cl.players[cl.playernum[0]].userinfo);

        // get answer
        SPipe_ReadChar(f_read, &answer);
        SPipe_ReadString(f_read, name, 64);
        SPipe_ReadUlong(f_read, &crc);

		if (answer == SECURE_ANSWER_OK)
		{
			// reply
			Cbuf_AddText("say ", RESTRICT_LOCAL);
			Cbuf_AddText(name, RESTRICT_LOCAL);
			if (*sr)
				Cbuf_AddText(va("/%s/%s", q_renderername, sr), RESTRICT_LOCAL);//extra info
			else
				Cbuf_AddText(va("/%s", q_renderername), RESTRICT_LOCAL);//extra info
			Cbuf_AddText(" ", RESTRICT_LOCAL);
			Cbuf_AddText(va("%04x", query_crc), RESTRICT_LOCAL);
			Cbuf_AddText(va("%08x", crc), RESTRICT_LOCAL);
			Cbuf_AddText("\n", RESTRICT_LOCAL);
			return;
		}
	}

	if (*sr)
		Cbuf_AddText (va("say "DISTRIBUTION"Quake v%4.3f-%i "PLATFORM"/%s/%s\n", VERSION, build_number(), q_renderername, sr), RESTRICT_RCON);
	else
		Cbuf_AddText (va("say "DISTRIBUTION"Quake v%4.3f-%i "PLATFORM"/%s\n", VERSION, build_number(), q_renderername), RESTRICT_RCON);
}

void Validation_Skins(void)
{
	extern cvar_t r_fullbrightSkins, r_fb_models;
	int percent = r_fullbrightSkins.value*100;
	if (percent < 0)
		percent = 0;
	if (percent > cls.allow_fbskins*100)
		percent = cls.allow_fbskins*100;
	if (percent)
		Cbuf_AddText(va("say all player skins %i%% fullbright%s\n", percent, r_fb_models.value?" (plus luma)":""), RESTRICT_LOCAL);
	else if (r_fb_models.value)
		Cbuf_AddText("say luma textures only\n", RESTRICT_LOCAL);
	else
		Cbuf_AddText("say Only cheaters use full bright skins\n", RESTRICT_LOCAL);
}

void Validation_Server(void)
{
	Cbuf_AddText(va("say server is %s\n", NET_AdrToString(cls.netchan.remote_address)), RESTRICT_LOCAL);
}

void Validation_CheckIfResponse(char *text)
{
	//client name, version type(os-renderer where it matters, os/renderer where renderer doesn't), 12 char hex crc
	int f_query_client;
	int i;
	char *crc;
	char *versionstring;

	if (!f_read)
		return;	//valid or not, we can't check it.

	if (!auth_validateclients.value)
		return;

	//do the parsing.
	{
		char *comp;
		int namelen;

		for (crc = text + strlen(text) - 1; crc > text; crc--)
			if (*crc > ' ')
				break;

		//find the crc.
		for (i = 0; i < 12; i++)
		{
			if (crc <= text)
				return;	//not enough chars.
			if ((*crc < '0' || *crc > '9') && (*crc < 'a' || *crc > 'f'))
				return;	//not a hex char.
			crc--;
		}

		//we now want 3 string seperated tokens, so the first starts at the fourth found ' ' + 1
		i = 4;
		for (comp = crc; ; comp--)
		{
			if (comp < text)
				return;
			if (*comp == ' ')
			{
				i--;
				if (!i)
					break;
			}

		}

		versionstring = comp+1;
		if (comp <= text)
			return;	//not enough space for the 'name:'
		if (*(comp-1) != ':')
			return;	//whoops. not a say.

		namelen = comp - text-1;

		for (f_query_client = 0; f_query_client < MAX_CLIENTS; f_query_client++)
		{
			if (strlen(cl.players[f_query_client].name) == namelen)
				if (!strncmp(cl.players[f_query_client].name, text, namelen))
					break;
		}
		if (f_query_client == MAX_CLIENTS)
			return; //looks like a validation, but it's not from a known client.

		crc++;
	}

	//now do the validation
	{
		f_query_t *query = NULL;
		int itemp;
		char buffer[10];
		unsigned short query_crc = 0;
		unsigned long	user_crc = 0;
		unsigned long	auth_crc = 0;
		char auth_answer;

		//easy lame way to get the crc from hex.
		Q_strncpyS(buffer, crc, 4);
		buffer[4] = '\0';
		itemp = 0;
		sscanf(buffer, "%x", &itemp);
		query_crc = (unsigned long) itemp;

		Q_strncpyS(buffer, crc+4, 8);
		buffer[8] = '\0';
		itemp = 0;
		sscanf(buffer, "%x", &itemp);
		user_crc = (unsigned long) itemp;

		//
		// find that query
		//
		for (i=f_last_query_pos; i > f_last_query_pos-F_QUERIES_REMEMBERED; i--)
		{
			if (query_crc == f_last_queries[i % F_QUERIES_REMEMBERED].crc  &&
				realtime - 5 < f_last_queries[i % F_QUERIES_REMEMBERED].timestamp)
			{
				query = &f_last_queries[i % F_QUERIES_REMEMBERED];
			}
		}

		if (query == NULL)
			return; // reply to unknown query

		if (!query->c_exist[f_query_client])
			return; // should never happen

		// write request
		SPipe_WriteChar(f_write, SECURE_CMD_CHECKVERSION2);
		SPipe_WriteString(f_write, query->query);
		SPipe_WriteString(f_write, query->serverinfo);
		SPipe_WriteString(f_write, query->c_userinfo[f_query_client]);
		SPipe_WriteString(f_write, versionstring);

		// get answer
		SPipe_ReadChar(f_read, &auth_answer);
		SPipe_ReadUlong(f_read, &auth_crc);

		if (auth_answer == SECURE_ANSWER_YES && auth_crc == user_crc)
		{
			Con_Printf("Authentication Successful.\n");
		}
		else
			Con_Printf("^1^bAUTHENTICATION FAILED.\n");
	}
}
