#include "xmpp.h"

#ifdef FILETRANSFERS

void XMPP_FT_Frame(jclient_t *jcl)
{
	static char *hex = "0123456789abcdef";
	char digest[20];
	char domain[sizeof(digest)*2+1];
	int j;
	char *req;
	struct ft_s *ft;
	for (ft = jcl->ft; ft; ft = ft->next)
	{
		if (ft->streamstatus == STRM_IDLE)
			continue;

		if (ft->stream < 0)
		{
			if (ft->nexthost > 0)
			{
				ft->nexthost--;
				ft->stream = pNet_TCPConnect(ft->streamhosts[ft->nexthost].host, ft->streamhosts[ft->nexthost].port);
				if (ft->stream == -1)
					continue;

				ft->streamstatus = STRM_AUTH;

				//'authenticate' with socks5 proxy. tell it that we only support 'authless'.
				pNet_Send(ft->stream, "\x05\x01\x00", 3);
			}
			else
			{
				JCL_AddClientMessagef(jcl,
					"<iq id='%s' to='%s' type='error'>"
						"<error type='cancel'>"
							"<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
						"</error>"
					"</iq>", ft->iqid, ft->with);
				ft->streamstatus = STRM_IDLE;
			}
			continue;
		}
		else
		{
			char data[8192];
			int len;
			if (ft->streamstatus == STRM_ACTIVE)
			{
				len = pNet_Recv(ft->stream, data, sizeof(data)-1);
				if (len > 0)
					pFS_Write(ft->file, data, len);
			}
			else
			{
				len = pNet_Recv(ft->stream, data, sizeof(data)-1);
				if (len > 0)
				{
					if (ft->streamstatus == STRM_AUTH)
					{
						if (len == 2 && data[0] == 0x05 && data[1] == 0x00)
						{
							//server has accepted us, woo.
							//sid+requester(them)+target(us)
							req = va("%s%s%s", ft->sid, ft->with, jcl->fulljid);
							SHA1(digest, sizeof(digest), req, strlen(req));
							//in hex
							for (req = domain, j=0; j < 20; j++)
							{
								*req++ = hex[(digest[j]>>4) & 0xf];
								*req++ = hex[(digest[j]>>0) & 0xf];
							}
							*req = 0;

							//connect with hostname(3).
							req = va("\x05\x01%c\x03""%c%s%c%c", 0, (int)strlen(domain), domain, 0, 0);
							pNet_Send(ft->stream, req, strlen(domain)+7);
							ft->streamstatus = STRM_AUTHED;
						}
						else
							len = -1;
					}
					else if (ft->streamstatus == STRM_AUTHED)
					{
						if (data[0] == 0x05 && data[1] == 0x00)
						{
							if (pFS_Open(ft->fname, &ft->file, 2) < 0)
							{
								len = -1;
								JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='error'/>", ft->iqid, ft->with);
							}
							else
							{
								ft->streamstatus = STRM_ACTIVE;
								JCL_AddClientMessagef(jcl,
										"<iq id='%s' to='%s' type='result'>"
											"<query xmlns='http://jabber.org/protocol/bytestreams' sid='%s'>"
												"<streamhost-used jid='%s'/>"
											"</query>"
										"</iq>", ft->iqid, ft->with, ft->sid, ft->streamhosts[ft->nexthost].jid);
							}
						}
						else
							len = -1;	//err, this is someone else...
					}
				}
			}

			if (len == -1)
			{
				pNet_Close(ft->stream);
				ft->stream = -1;
				if (ft->streamstatus == STRM_ACTIVE)
				{
					int size;
					if (ft->file != -1)
						pFS_Close(ft->file);
					ft->file = -1;

					size = pFS_Open(ft->fname, &ft->file, 1);
					if (ft->file != -1)
						pFS_Close(ft->file);
					if (size == ft->size)
					{
						Con_Printf("File Transfer Completed\n");
						continue;
					}
					else
					{
						Con_Printf("File Transfer Aborted by peer\n");
						continue;
					}
				}
				Con_Printf("File Transfer connection to %s:%i failed\n", ft->streamhosts[ft->nexthost].host, ft->streamhosts[ft->nexthost].port);
			}
		}
	}
}

void XMPP_FT_AcceptFile(jclient_t *jcl, int fileid, qboolean accept)
{
	struct ft_s *ft, **link;
	char *s;
	xmltree_t *repiq, *repsi, *c;

	for (link = &jcl->ft; (ft=*link); link = &(*link)->next)
	{
		if (ft->privateid == fileid)
			break;
	}
	if (!ft)
	{
		Con_Printf("File not known\n");
		return;
	}

	if (!accept)
	{
		Con_Printf("Declining file \"%s\" from \"%s\" (%i bytes)\n", ft->fname, ft->with, ft->size);

		if (!ft->allowed)
		{
			JCL_AddClientMessagef(jcl,
						"<iq type='error' to='%s' id='%s'>"
							"<error type='cancel'>"
							"<forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
								"<text>Offer Declined</text>"
							"</error>"
						"</iq>", ft->with, ft->iqid);
		}
		else
		{
			//FIXME: send a proper cancel
		}

		if (ft->file != -1)
			pFS_Close(ft->file);
		*link = ft->next;
		free(ft);
	}
	else
	{
		Con_Printf("Receiving file \"%s\" from \"%s\" (%i bytes)\n", ft->fname, ft->with, ft->size);
		ft->allowed = true;

		//generate a response.
		//FIXME: we ought to delay response until after we prompt.
		repiq = XML_CreateNode(NULL, "iq", "", "");
		XML_AddParameter(repiq, "type", "result");
		XML_AddParameter(repiq, "to", ft->with);
		XML_AddParameter(repiq, "id", ft->iqid);
		repsi = XML_CreateNode(repiq, "si", "http://jabber.org/protocol/si", "");
		XML_CreateNode(repsi, "file", "http://jabber.org/protocol/si/profile/file-transfer", "");	//I don't really get the point of this.
		c = XML_CreateNode(repsi, "feature", "http://jabber.org/protocol/feature-neg", "");
		c = XML_CreateNode(c, "x", "jabber:x:data", "");
		XML_AddParameter(c, "type", "submit");
		c = XML_CreateNode(c, "field", "", "");
		XML_AddParameter(c, "var", "stream-method");
		if (ft->method == FT_IBB)
			c = XML_CreateNode(c, "value", "", "http://jabber.org/protocol/ibb");
		else if (ft->method == FT_BYTESTREAM)
			c = XML_CreateNode(c, "value", "", "http://jabber.org/protocol/bytestreams");

		s = XML_GenerateString(repiq, false);
		JCL_AddClientMessageString(jcl, s);
		free(s);
		XML_Destroy(repiq);
	}
}

static qboolean XMPP_FT_IBBChunked(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
	const char *from = XML_GetParameter(x, "from", jcl->domain);
	struct ft_s *ft = iq->usrptr, **link, *v;
	for (link = &jcl->ft; (v=*link); link = &(*link)->next)
	{
		if (v == ft && !strcmp(ft->with, from))
		{
			//its still valid
			if (x)
			{
				char *base64;
				char rawbuf[4096];
				int sz;
				sz = pFS_Read(ft->file, rawbuf, ft->blocksize);
				Base64_Add(rawbuf, sz);
				base64 = Base64_Finish();

				if (sz > 0)
				{
					ft->sizedone += sz;
					if (ft->sizedone == ft->size)
						ft->eof = true;
				}

				if (sz && strlen(base64))
				{
					x = XML_CreateNode(NULL, "data", "http://jabber.org/protocol/ibb", base64);
					XML_AddParameteri(x, "seq", ft->seq++);
					XML_AddParameter(x, "sid", ft->sid);
					JCL_SendIQNode(jcl, XMPP_FT_IBBChunked, "set", from, x, true)->usrptr = ft;
					return true;
				}
				//else eof
			}

			//errored or ended

			if (x)
				Con_Printf("Transfer of %s to %s completed\n", ft->fname, ft->with);
			else
				Con_Printf("%s aborted %s\n", ft->with, ft->fname);
			x = XML_CreateNode(NULL, "close", "http://jabber.org/protocol/ibb", "");
			XML_AddParameter(x, "sid", ft->sid);
			JCL_SendIQNode(jcl, NULL, "set", from, x, true)->usrptr = ft;

			//errored
			if (ft->file != -1)
				pFS_Close(ft->file);
			*link = ft->next;
			free(ft);
			return true;
		}
	}
	return true;	//the ack can come after the bytestream has already finished sending. don't warn about that.
}
static qboolean XMPP_FT_IBBBegun(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
	struct ft_s *ft = iq->usrptr, **link, *v;
	const char *from = XML_GetParameter(x, "from", jcl->domain);
	for (link = &jcl->ft; (v=*link); link = &(*link)->next)
	{
		if (v == ft && !strcmp(ft->with, from))
		{
			//its still valid
			if (!x)
			{
				Con_Printf("%s aborted %s\n", ft->with, ft->fname);
				//errored
				if (ft->file != -1)
					pFS_Close(ft->file);
				*link = ft->next;
				free(ft);
			}
			else
			{
				ft->begun = true;
				ft->method = FT_IBB;
				XMPP_FT_IBBChunked(jcl, x, iq);
			}
			return true;
		}
	}
	return false;
}
qboolean XMPP_FT_OfferAcked(jclient_t *jcl, xmltree_t *x, struct iq_s *iq)
{
	struct ft_s *ft = iq->usrptr, **link, *v;
	const char *from = XML_GetParameter(x, "from", jcl->domain);
	for (link = &jcl->ft; (v=*link); link = &(*link)->next)
	{
		if (v == ft && !strcmp(ft->with, from))
		{
			//its still valid
			if (!x)
			{
				Con_Printf("%s doesn't want %s\n", ft->with, ft->fname);
				//errored
				if (ft->file != -1)
					pFS_Close(ft->file);
				*link = ft->next;
				free(ft);
			}
			else
			{
				xmltree_t *xo;
				Con_Printf("%s accepted %s\n", ft->with, ft->fname);
				xo = XML_CreateNode(NULL, "open", "http://jabber.org/protocol/ibb", "");
				XML_AddParameter(xo, "sid", ft->sid);
				XML_AddParameteri(xo, "block-size", ft->blocksize);
				//XML_AddParameter(xo, "stanza", "iq");

				JCL_SendIQNode(jcl, XMPP_FT_IBBBegun, "set", XML_GetParameter(x, "from", jcl->domain), xo, true)->usrptr = ft;
			}
			return true;
		}
	}
	return false;
}

void XMPP_FT_SendFile(jclient_t *jcl, const char *console, const char *to, const char *fname)
{
	xmltree_t *xsi, *xfile, *c;
	struct ft_s *ft;

	Con_SubPrintf(console, "Offering %s to %s.\n", fname, to);

	ft = malloc(sizeof(*ft));
	memset(ft, 0, sizeof(*ft));
	ft->stream = -1;
	ft->next = jcl->ft;
	jcl->ft = ft;
	ft->allowed = true;
	ft->transmitting = true;
	ft->blocksize = 4096;
	Q_strlcpy(ft->fname, fname, sizeof(ft->fname));
	Q_snprintf(ft->sid, sizeof(ft->sid), "%x%s", rand(), ft->fname);
	Q_strlcpy(ft->md5hash, "", sizeof(ft->md5hash));
	ft->size = pFS_Open(ft->fname, &ft->file, 1);
	ft->with = strdup(to);
	ft->method = FT_IBB;
	ft->begun = false;

	//generate an offer.
	xsi = XML_CreateNode(NULL, "si", "http://jabber.org/protocol/si", "");
		XML_AddParameter(xsi, "profile", "http://jabber.org/protocol/si/profile/file-transfer");
		XML_AddParameter(xsi, "id", ft->sid);
	//XML_AddParameter(xsi, "mime-type", "text/plain");
	xfile = XML_CreateNode(xsi, "file", "http://jabber.org/protocol/si/profile/file-transfer", "");	//I don't really get the point of this.
		XML_AddParameter(xfile, "name", ft->fname);
		XML_AddParameteri(xfile, "size", ft->size);
	c = XML_CreateNode(xsi, "feature", "http://jabber.org/protocol/feature-neg", "");
		c = XML_CreateNode(c, "x", "jabber:x:data", "");
			XML_AddParameter(c, "type", "form");
			c = XML_CreateNode(c, "field", "", "");
				XML_AddParameter(c, "var", "stream-method");
				XML_AddParameter(c, "type", "listsingle");
					XML_CreateNode(XML_CreateNode(c, "option", "", ""), "value", "", "http://jabber.org/protocol/ibb");
//					XML_CreateNode(XML_CreateNode(c, "option", "", ""), "value", "", "http://jabber.org/protocol/bytestreams");

	JCL_SendIQNode(jcl, XMPP_FT_OfferAcked, "set", to, xsi, true)->usrptr = ft;
}

qboolean XMPP_FT_ParseIQSet(jclient_t *jcl, const char *iqfrom, const char *iqid, xmltree_t *tree)
{
	xmltree_t *ot;

	//if file transfers are not enabled in this build, reject all iqs
	if (!(jcl->enabledcapabilities & CAP_SIFT))
		return false;

	ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/bytestreams", "query", 0);
	if (ot)
	{
		xmltree_t *c;
		struct ft_s *ft;
		const char *sid = XML_GetParameter(ot, "sid", "");
		for (ft = jcl->ft; ft; ft = ft->next)
		{
			if (!strcmp(ft->sid, sid) && !strcmp(ft->with, iqfrom))
			{
				if (ft->allowed && !ft->begun && ft->transmitting == false)
				{
					const char *jid;
					const char *host;
					int port;
					const char *mode = XML_GetParameter(ot, "mode", "tcp");
					int i;
					if (strcmp(mode, "tcp"))
						break;
					for (i = 0; ; i++)
					{
						c = XML_ChildOfTree(ot, "streamhost", i);
						if (!c)
							break;

						jid = XML_GetParameter(c, "jid", "");
						host = XML_GetParameter(c, "host", "");
						port = atoi(XML_GetParameter(c, "port", "0"));
						if (port <= 0 || port > 65535)
							continue;
						if (ft->nexthost < sizeof(ft->streamhosts) / sizeof(ft->streamhosts[ft->nexthost]))
						{
							Q_strlcpy(ft->streamhosts[ft->nexthost].jid, jid, sizeof(ft->streamhosts[ft->nexthost].jid));
							Q_strlcpy(ft->streamhosts[ft->nexthost].host, host, sizeof(ft->streamhosts[ft->nexthost].host));
							ft->streamhosts[ft->nexthost].port = port;
							ft->nexthost++;
						}
					}
					ft->streamstatus = STRM_AUTH;
					//iq gets acked when we finished connecting to the proxy. *then* the data can flow
					Q_strlcpy(ft->iqid, iqid, sizeof(ft->iqid));
					return true;
				}
			}
		}
		JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='error'/>", iqid, iqfrom);
		return true;
	}

	ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "open", 0);
	if (ot)
	{
		struct ft_s *ft;
		const char *sid = XML_GetParameter(ot, "sid", "");
		int blocksize = atoi(XML_GetParameter(ot, "block-size", "4096"));	//technically this is required.
		const char *stanza = XML_GetParameter(ot, "stanza", "iq");
		for (ft = jcl->ft; ft; ft = ft->next)
		{
			if (!strcmp(ft->sid, sid) && !strcmp(ft->with, iqfrom))
			{
				if (ft->allowed && !ft->begun && ft->transmitting == false)
				{
					if (blocksize > 65536 || strcmp(stanza, "iq"))
					{	//blocksize: MUST NOT be greater than 65535
						JCL_AddClientMessagef(jcl, 
								"<iq id='%s' to='%s' type='error'>"
									"<error type='modify'>"
										"<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
									"</error>"
								"</iq>"
								, iqid, iqfrom);
					}
					else if (blocksize > 4096)
					{	//ask for smaller chunks
						JCL_AddClientMessagef(jcl, 
							"<iq id='%s' to='%s' type='error'>"
								"<error type='modify'>"
									"<resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
								"</error>"
							"</iq>"
							, iqid, iqfrom);
					}
					else
					{	//it looks okay
						pFS_Open(ft->fname, &ft->file, 2);
						ft->method = FT_IBB;
						ft->blocksize = blocksize;
						ft->begun = true;
						//if its okay...
						JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", iqid, iqfrom);
					}
					break;
				}
			}
		}
		return false;
	}
	ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "close", 0);
	if (ot)
	{
		struct ft_s **link, *ft;
		const char *sid = XML_GetParameter(ot, "sid", "");
		for (link = &jcl->ft; *link; link = &(*link)->next)
		{
			ft = *link;
			if (!strcmp(ft->sid, sid) && !strcmp(ft->with, iqfrom))
			{
				if (ft->begun && ft->method == FT_IBB)
				{
					int size;
					if (ft->file != -1)
						pFS_Close(ft->file);
					if (ft->transmitting)
					{
						if (ft->eof)
							Con_Printf("Sent \"%s\" to \"%s\"\n", ft->fname, ft->with);
						else
							Con_Printf("%s aborted transfer of \"%s\"\n", iqfrom, ft->fname);
					}
					else
					{
						size = pFS_Open(ft->fname, &ft->file, 1);
						if (ft->file != -1)
							pFS_Close(ft->file);
						if (size == ft->size)
							Con_Printf("Received file \"%s\" successfully\n", ft->fname);
						else
							Con_Printf("%s aborted transfer of \"%s\"\n", iqfrom, ft->fname);
					}
					*link = ft->next;
					free(ft);
					//if its okay...
					JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", iqid, iqfrom);
					return true;
				}
			}
		}
		return false;	//some kind of error
	}
	ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/ibb", "data", 0);
	if (ot)
	{
		char block[65536];
		const char *sid = XML_GetParameter(ot, "sid", "");
//		unsigned short seq = atoi(XML_GetParameter(ot, "seq", "0"));
		int blocksize;
		struct ft_s *ft;
		//FIXME: validate the sequence!
		for (ft = jcl->ft; ft; ft = ft->next)
		{
			if (!strcmp(ft->sid, sid) && !ft->transmitting)
			{
				blocksize = Base64_Decode(block, sizeof(block), ot->body, strlen(ot->body));
				if (blocksize && blocksize <= ft->blocksize)
				{
					pFS_Write(ft->file, block, blocksize);
					JCL_AddClientMessagef(jcl, "<iq id='%s' to='%s' type='result'/>", iqid, iqfrom);
					return true;
				}
				else
					Con_Printf("XMPP: Invalid blocksize in file transfer from %s\n", iqfrom);
				break;
			}
		}
		return true;
	}

	ot = XML_ChildOfTreeNS(tree, "http://jabber.org/protocol/si", "si", 0);
	if (ot)
	{
		const char *profile = XML_GetParameter(ot, "profile", "");

		if (!strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer"))
		{
//			char *mimetype = XML_GetParameter(ot, "mime-type", "text/plain");
			const char *sid = XML_GetParameter(ot, "id", "");
			xmltree_t *file = XML_ChildOfTreeNS(ot, "http://jabber.org/protocol/si/profile/file-transfer", "file", 0);
			const char *fname = XML_GetParameter(file, "name", "file.txt");
//			char *date = XML_GetParameter(file, "date", "");
			const char *md5hash = XML_GetParameter(file, "hash", "");
			int fsize = strtoul(XML_GetParameter(file, "size", "0"), NULL, 0);
//			char *desc = XML_GetChildBody(file, "desc", "");
			char authlink[512];
			char denylink[512];
			
			//file transfer offer
			struct ft_s *ft = malloc(sizeof(*ft) + strlen(iqfrom)+1);
			memset(ft, 0, sizeof(*ft));
			ft->stream = -1;
			ft->next = jcl->ft;
			jcl->ft = ft;
			ft->privateid = ++jcl->privateidseq;

			ft->transmitting = false;
			Q_strlcpy(ft->iqid, iqid, sizeof(ft->iqid));
			Q_strlcpy(ft->sid, sid, sizeof(ft->sid));
			Q_strlcpy(ft->fname, fname, sizeof(ft->sid));
			Base64_Decode(ft->md5hash, sizeof(ft->md5hash), md5hash, strlen(md5hash));
			ft->size = fsize;
			ft->file = -1;
			ft->with = (char*)(ft+1);
			strcpy(ft->with, iqfrom);
			ft->method = (fsize > 1024*128)?FT_BYTESTREAM:FT_IBB;	//favour bytestreams for large files. for small files, just use ibb as it saves sorting out proxies.
			ft->begun = false;

			JCL_GenLink(jcl, authlink, sizeof(authlink), "fauth", iqfrom, NULL, va("%i", ft->privateid), "%s", "Accept");
			JCL_GenLink(jcl, denylink, sizeof(denylink), "fdeny", iqfrom, NULL, va("%i", ft->privateid), "%s", "Deny");

			Con_Printf("%s has offered to send you \"%s\" (%i bytes). %s %s\n", iqfrom, fname, fsize, authlink, denylink);
		}
		else
		{
			//profile not understood
			JCL_AddClientMessagef(jcl,
					"<iq type='error' to='%s' id='%s'>"
						"<error code='400' type='cancel'>"
							"<bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
							"<bad-profile xmlns='http://jabber.org/protocol/si'/>"
						"</error>"
					"</iq>", iqfrom, iqid);
		}
		return true;
	}

	return false;
}
#endif
