/*

Jabber Protocol Plugin for Miranda NG

Copyright (c) 2002-04  Santithorn Bunchua
Copyright (c) 2005-12  George Hazan
Copyright (�) 2012-16 Miranda NG project

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.

*/

#include "stdafx.h"
#include "jabber_caps.h"

#define JABBER_NETWORK_BUFFER_SIZE 2048

void __cdecl CJabberProto::FileReceiveThread(filetransfer *ft)
{
	ThreadData info(this, NULL);

	debugLogA("Thread started: type=file_receive server='%s' port='%d'", ft->httpHostName, ft->httpPort);

	ft->type = FT_OOB;

	NETLIBOPENCONNECTION nloc = { 0 };
	nloc.cbSize = sizeof(nloc);
	nloc.szHost = ft->httpHostName;
	nloc.wPort = ft->httpPort;
	info.s = (HANDLE)CallService(MS_NETLIB_OPENCONNECTION, (WPARAM)m_hNetlibUser, (LPARAM)&nloc);
	if (info.s == NULL) {
		debugLogA("Connection failed (%d), thread ended", WSAGetLastError());
		ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
	}
	else {
		ft->s = info.s;

		char buffer[JABBER_NETWORK_BUFFER_SIZE];
		int datalen = mir_snprintf(buffer, "GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", ft->httpPath, ft->httpHostName);
		info.send(buffer, datalen);
		ft->state = FT_CONNECTING;

		debugLogA("Entering file_receive recv loop");

		datalen = 0;
		while (ft->state != FT_DONE && ft->state != FT_ERROR) {
			int recvResult, bytesParsed;

			debugLogA("Waiting for data...");
			recvResult = info.recv(buffer+datalen, JABBER_NETWORK_BUFFER_SIZE-datalen);
			if (recvResult <= 0)
				break;
			datalen += recvResult;

			bytesParsed = FileReceiveParse(ft, buffer, datalen);
			if (bytesParsed < datalen)
				memmove(buffer, buffer+bytesParsed, datalen-bytesParsed);
			datalen -= bytesParsed;
		}

		ft->s = NULL;

		if (ft->state == FT_DONE || (ft->state == FT_RECEIVING && ft->std.currentFileSize == 0))
			ft->complete();

		debugLogA("Thread ended: type=file_receive server='%s'", ft->httpHostName);
	}
	delete ft;
}

int CJabberProto::FileReceiveParse(filetransfer *ft, char* buffer, int datalen)
{
	char* p, *q, *eob;
	int num, code;

	eob = buffer + datalen;
	p = buffer;
	num = 0;
	while (true) {
		if (ft->state == FT_CONNECTING || ft->state == FT_INITIALIZING) {
			for (q = p; q + 1 < eob && (*q != '\r' || *(q + 1) != '\n'); q++);
			if (q + 1 >= eob)
				break;

			ptrA str(mir_strndup(p, size_t(q - p)));
			if (str == NULL) {
				ft->state = FT_ERROR;
				break;
			}

			debugLogA("FT Got: %s", str);
			if (ft->state == FT_CONNECTING) {
				// looking for "HTTP/1.1 200 OK"
				if (sscanf(str, "HTTP/%*d.%*d %d %*s", &code) == 1 && code == 200) {
					ft->state = FT_INITIALIZING;
					ft->std.currentFileSize = -1;
					debugLogA("Change to FT_INITIALIZING");
					ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, ft, 0);
				}
			}
			else {	// FT_INITIALIZING
				if (str[0] == '\0') {
					TCHAR *s;
					if ((s = _tcsrchr(ft->httpPath, '/')) != NULL)
						s++;
					else
						s = ft->httpPath;
					ft->std.tszCurrentFile = mir_tstrdup(s);
					JabberHttpUrlDecode(ft->std.tszCurrentFile);
					if (ft->create() == -1) {
						ft->state = FT_ERROR;
						break;
					}
					ft->state = FT_RECEIVING;
					ft->std.currentFileProgress = 0;
					debugLogA("Change to FT_RECEIVING");
				}
				else if (char *s = strchr(str, ':')) {
					*s = '\0';
					if (!mir_strcmp(str, "Content-Length"))
						ft->std.totalBytes = ft->std.currentFileSize = _atoi64(s + 1);
				}
			}

			q += 2;
			num += (q - p);
			p = q;
		}
		else if (ft->state == FT_RECEIVING) {
			int bufferSize, writeSize;
			__int64 remainingBytes;

			if (ft->std.currentFileProgress < ft->std.currentFileSize) {
				bufferSize = eob - p;
				remainingBytes = ft->std.currentFileSize - ft->std.currentFileProgress;
				if (remainingBytes < bufferSize)
					writeSize = remainingBytes;
				else
					writeSize = bufferSize;
				if (_write(ft->fileId, p, writeSize) != writeSize) {
					debugLogA("_write() error");
					ft->state = FT_ERROR;
				}
				else {
					ft->std.currentFileProgress += writeSize;
					ft->std.totalProgress += writeSize;
					ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->std);
					if (ft->std.currentFileProgress == ft->std.currentFileSize)
						ft->state = FT_DONE;
				}
			}
			num = datalen;
			break;
		}
		else break;
	}

	return num;
}

void JabberFileServerConnection(JABBER_SOCKET hConnection, DWORD /*dwRemoteIP*/, void* extra)
{
	CJabberProto *ppro = (CJabberProto*)extra;

	NETLIBCONNINFO connInfo = { sizeof(connInfo) };
	CallService(MS_NETLIB_GETCONNECTIONINFO, (WPARAM)hConnection, (LPARAM)&connInfo);

	TCHAR szPort[10];
	mir_sntprintf(szPort, _T("%d"), connInfo.wPort);
	ppro->debugLogA("File server incoming connection accepted: %s", connInfo.szIpPort);

	JABBER_LIST_ITEM *item = ppro->ListGetItemPtr(LIST_FILE, szPort);
	if (item == NULL) {
		ppro->debugLogA("No file is currently served, file server connection closed.");
		Netlib_CloseHandle(hConnection);
		return;
	}

	filetransfer *ft = item->ft;
	JABBER_SOCKET slisten = ft->s;
	ft->s = hConnection;
	ppro->debugLogA("Set ft->s to %d (saving %d)", hConnection, slisten);

	char* buffer = (char*)mir_alloc(JABBER_NETWORK_BUFFER_SIZE + 1);
	if (buffer == NULL) {
		ppro->debugLogA("Cannot allocate network buffer, file server connection closed.");
		Netlib_CloseHandle(hConnection);
		ft->state = FT_ERROR;
		if (ft->hFileEvent != NULL)
			SetEvent(ft->hFileEvent);
		return;
	}

	ppro->debugLogA("Entering recv loop for this file connection... (ft->s is hConnection)");
	int datalen = 0;
	while (ft->state != FT_DONE && ft->state != FT_ERROR) {
		int recvResult, bytesParsed;

		recvResult = Netlib_Recv(hConnection, buffer + datalen, JABBER_NETWORK_BUFFER_SIZE - datalen, 0);
		if (recvResult <= 0)
			break;
		datalen += recvResult;

		buffer[datalen] = '\0';
		ppro->debugLogA("RECV:%s", buffer);

		bytesParsed = ppro->FileSendParse(hConnection, ft, buffer, datalen);
		if (bytesParsed < datalen)
			memmove(buffer, buffer + bytesParsed, datalen - bytesParsed);
		datalen -= bytesParsed;
	}

	ppro->debugLogA("Closing connection for this file transfer... (ft->s is now hBind)");
	Netlib_CloseHandle(hConnection);
	ft->s = slisten;
	ppro->debugLogA("ft->s is restored to %d", ft->s);
	if (ft->hFileEvent != NULL)
		SetEvent(ft->hFileEvent);
	mir_free(buffer);
}

void __cdecl CJabberProto::FileServerThread(filetransfer *ft)
{
	debugLogA("Thread started: type=file_send");

	ThreadData info(this, NULL);
	ft->type = FT_OOB;

	NETLIBBIND nlb = { 0 };
	nlb.cbSize = sizeof(NETLIBBIND);
	nlb.pfnNewConnectionV2 = JabberFileServerConnection;
	nlb.pExtra = this;
	nlb.wPort = 0;	// Use user-specified incoming port ranges, if available
	info.s = (HANDLE)CallService(MS_NETLIB_BINDPORT, (WPARAM)m_hNetlibUser, (LPARAM)&nlb);
	if (info.s == NULL) {
		debugLogA("Cannot allocate port to bind for file server thread, thread ended.");
		ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
		delete ft;
		return;
	}

	ft->s = info.s;
	debugLogA("ft->s = %d", info.s);

	HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	ft->hFileEvent = hEvent;

	TCHAR szPort[20];
	mir_sntprintf(szPort, _T("%d"), nlb.wPort);
	JABBER_LIST_ITEM *item = ListAdd(LIST_FILE, szPort);
	item->ft = ft;

	TCHAR *ptszResource = ListGetBestClientResourceNamePtr(ft->jid);
	if (ptszResource != NULL) {
		ft->state = FT_CONNECTING;
		for (int i = 0; i < ft->std.totalFiles && ft->state != FT_ERROR && ft->state != FT_DENIED; i++) {
			ft->std.currentFileNumber = i;
			ft->state = FT_CONNECTING;
			if (ft->httpPath) mir_free(ft->httpPath);
			ft->httpPath = NULL;

			TCHAR *p;
			if ((p = _tcschr(ft->std.ptszFiles[i], '\\')) != NULL)
				p++;
			else
				p = ft->std.ptszFiles[i];

			ptrA pFileName(mir_urlEncode(T2Utf(p)));
			if (pFileName != NULL) {
				ft->szId = JabberId2string(SerialNext());

				ptrA myAddr;
				if (m_options.BsDirect && m_options.BsDirectManual)
					myAddr = getStringA("BsDirectAddr");
				if (myAddr == NULL)
					myAddr = (char*)CallService(MS_NETLIB_ADDRESSTOSTRING, 1, nlb.dwExternalIP);

				char szAddr[256];
				mir_snprintf(szAddr, "http://%s:%d/%s", myAddr, nlb.wPort, pFileName);

				size_t len = mir_tstrlen(ptszResource) + mir_tstrlen(ft->jid) + 2;
				TCHAR *fulljid = (TCHAR *)alloca(sizeof(TCHAR) * len);
				mir_sntprintf(fulljid, len, _T("%s/%s"), ft->jid, ptszResource);

				XmlNodeIq iq(_T("set"), ft->szId, fulljid);
				HXML query = iq << XQUERY(JABBER_FEAT_OOB);
				query << XCHILD(_T("url"), _A2T(szAddr));
				query << XCHILD(_T("desc"), ft->szDescription);
				m_ThreadInfo->send(iq);

				debugLogA("Waiting for the file to be sent...");
				WaitForSingleObject(hEvent, INFINITE);
			}
			debugLogA("File sent, advancing to the next file...");
			ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
		}
		CloseHandle(hEvent);
		ft->hFileEvent = NULL;
		debugLogA("Finish all files");
	}

	ft->s = NULL;
	debugLogA("ft->s is NULL");

	ListRemove(LIST_FILE, szPort);

	switch (ft->state) {
	case FT_DONE:
		debugLogA("Finish successfully");
		ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft, 0);
		break;
	case FT_DENIED:
		ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DENIED, ft, 0);
		break;
	default: // FT_ERROR:
		debugLogA("Finish with errors");
		ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
		break;
	}

	debugLogA("Thread ended: type=file_send");
	delete ft;
}

int CJabberProto::FileSendParse(JABBER_SOCKET s, filetransfer *ft, char* buffer, int datalen)
{
	char* p, *q, *eob;
	char* str;
	int num;
	int currentFile;
	int fileId;
	int numRead;

	eob = buffer + datalen;
	p = buffer;
	num = 0;
	while (ft->state == FT_CONNECTING || ft->state == FT_INITIALIZING) {
		for (q = p; q + 1 < eob && (*q != '\r' || *(q + 1) != '\n'); q++);
		if (q + 1 >= eob)
			break;
		if ((str = (char*)mir_alloc(q - p + 1)) == NULL) {
			ft->state = FT_ERROR;
			break;
		}
		strncpy_s(str, q - p, p, _TRUNCATE);
		str[q - p] = '\0';
		debugLogA("FT Got: %s", str);
		if (ft->state == FT_CONNECTING) {
			// looking for "GET filename.ext HTTP/1.1"
			if (!strncmp(str, "GET ", 4)) {
				char *t;
				for (t = str + 4; *t != '\0' && *t != ' '; t++);
				*t = '\0';
				for (t = str + 4; *t != '\0' && *t == '/'; t++);
				ft->httpPath = mir_a2t(t);
				JabberHttpUrlDecode(ft->httpPath);
				ft->state = FT_INITIALIZING;
				debugLogA("Change to FT_INITIALIZING");
			}
		}
		else {	// FT_INITIALIZING
			if (str[0] == '\0') {
				struct _stati64 statbuf;

				mir_free(str);
				num += 2;

				currentFile = ft->std.currentFileNumber;
				TCHAR *t = _tcsrchr(ft->std.ptszFiles[currentFile], '\\');
				if (t != NULL)
					t++;
				else
					t = ft->std.ptszFiles[currentFile];

				if (ft->httpPath == NULL || mir_tstrcmp(ft->httpPath, t)) {
					if (ft->httpPath == NULL)
						debugLogA("Requested file name does not matched (httpPath == NULL)");
					else
						debugLog(_T("Requested file name does not matched ('%s' vs. '%s')"), ft->httpPath, t);
					ft->state = FT_ERROR;
					break;
				}
				debugLog(_T("Sending [%s]"), ft->std.ptszFiles[currentFile]);
				_tstati64(ft->std.ptszFiles[currentFile], &statbuf);	// file size in statbuf.st_size
				if ((fileId = _topen(ft->std.ptszFiles[currentFile], _O_BINARY | _O_RDONLY)) < 0) {
					debugLogA("File cannot be opened");
					ft->state = FT_ERROR;
					mir_free(ft->httpPath);
					ft->httpPath = NULL;
					break;
				}

				char fileBuffer[2048];
				int bytes = mir_snprintf(fileBuffer, "HTTP/1.1 200 OK\r\nContent-Length: %I64u\r\n\r\n", statbuf.st_size);
				WsSend(s, fileBuffer, bytes, MSG_DUMPASTEXT);

				ft->std.flags |= PFTS_SENDING;
				ft->std.currentFileProgress = 0;
				debugLogA("Sending file data...");

				while ((numRead = _read(fileId, fileBuffer, 2048)) > 0) {
					if (Netlib_Send(s, fileBuffer, numRead, 0) != numRead) {
						ft->state = FT_ERROR;
						break;
					}
					ft->std.currentFileProgress += numRead;
					ft->std.totalProgress += numRead;
					ProtoBroadcastAck(ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->std);
				}
				_close(fileId);
				if (ft->state != FT_ERROR)
					ft->state = FT_DONE;
				debugLogA("Finishing this file...");
				mir_free(ft->httpPath);
				ft->httpPath = NULL;
				break;
		}	}

		mir_free(str);
		q += 2;
		num += (q-p);
		p = q;
	}

	return num;
}

/////////////////////////////////////////////////////////////////////////////////////////
// filetransfer class members

filetransfer::filetransfer(CJabberProto *proto)
{
	memset(this, 0, sizeof(filetransfer));
	ppro = proto;
	fileId = -1;
	std.cbSize = sizeof(std);
	std.flags = PFTS_TCHAR;
}

filetransfer::~filetransfer()
{
	ppro->debugLogA("Destroying file transfer session %08p", this);

	if (!bCompleted)
		ppro->ProtoBroadcastAck(std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, this, 0);

	close();

	if (hWaitEvent != INVALID_HANDLE_VALUE)
		CloseHandle(hWaitEvent);

	mir_free(szId);
	mir_free(jid);
	mir_free(sid);
	mir_free(fileSize);
	mir_free(httpHostName);
	mir_free(httpPath);
	mir_free(szDescription);

	mir_free(std.tszWorkingDir);
	mir_free(std.tszCurrentFile);

	if (std.ptszFiles) {
		for (int i = 0; i < std.totalFiles; i++)
			mir_free(std.ptszFiles[i]);
		mir_free(std.ptszFiles);
}	}

void filetransfer::close()
{
	if (fileId != -1) {
		_close(fileId);
		fileId = -1;
}	}

void filetransfer::complete()
{
	close();

	bCompleted = true;
	ppro->ProtoBroadcastAck(std.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, this, 0);
}

int filetransfer::create()
{
	if (fileId != -1)
		return fileId;

	TCHAR filefull[MAX_PATH];
	mir_sntprintf(filefull, _T("%s\\%s"), std.tszWorkingDir, std.tszCurrentFile);
	replaceStrT(std.tszCurrentFile, filefull);

	if (hWaitEvent != INVALID_HANDLE_VALUE)
		CloseHandle(hWaitEvent);
	hWaitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

	if (ppro->ProtoBroadcastAck(std.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, this, (LPARAM)&std))
		WaitForSingleObject(hWaitEvent, INFINITE);

	if (fileId == -1) {
		ppro->debugLog(_T("Saving to [%s]"), std.tszCurrentFile);
		fileId = _topen(std.tszCurrentFile, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE);
	}

	if (fileId == -1)
		ppro->debugLog(_T("Cannot create file '%s' during a file transfer"), filefull);
	else if (std.currentFileSize != 0)
		_chsize(fileId, std.currentFileSize);

	return fileId;
}