/*
Plugin of Miranda IM for communicating with users of the AIM protocol.
Copyright (c) 2008-2009 Boris Krasnovskiy
Copyright (C) 2005-2006 Aaron Myles Landwehr

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, see <http://www.gnu.org/licenses/>.
*/
#include "aim.h"
#include "file.h"

#pragma pack(push, 1)
struct oft2//oscar file transfer 2 class- See On_Sending_Files_via_OSCAR.pdf
{
	char protocol_version[4];//4
	unsigned short length;//6
	unsigned short type;//8
	unsigned char icbm_cookie[8];//16
	unsigned short encryption;//18
	unsigned short compression;//20
	unsigned short total_files;//22
	unsigned short num_files_left;//24
	unsigned short total_parts;//26
	unsigned short parts_left;//28
	unsigned long total_size;//32
	unsigned long size;//36
	unsigned long mod_time;//40
	unsigned long checksum;//44
	unsigned long recv_RFchecksum;//48
	unsigned long RFsize;//52
	unsigned long creation_time;//56
	unsigned long RFchecksum;//60
	unsigned long recv_bytes;//64
	unsigned long recv_checksum;//68
	unsigned char idstring[32];//100
	unsigned char flags;//101
	unsigned char list_name_offset;//102
	unsigned char list_size_offset;//103
	unsigned char dummy[69];//172
	unsigned char mac_info[16];//188
	unsigned short encoding;//190
	unsigned short sub_encoding;//192
	unsigned char filename[64];//256
 };

#pragma pack(pop)

bool send_init_oft2(file_transfer *ft, char* file)
{
	aimString astr(file);

	unsigned short len = max(0x100, 0xc0 + astr.getTermSize());

	oft2 *oft = (oft2*)alloca(len);
	memset(oft, 0, len);

	memcpy(oft->protocol_version, "OFT2", 4);
	oft->length           = _htons(len);
	oft->type             = 0x0101;
	oft->total_files      = _htons(ft->pfts.totalFiles);
	oft->num_files_left   = _htons(ft->pfts.totalFiles - ft->pfts.currentFileNumber);
	oft->total_parts      = _htons(1);
	oft->parts_left       = _htons(1);
	oft->total_size       = _htonl(ft->pfts.totalBytes);
	oft->size             = _htonl(ft->pfts.currentFileSize);
	oft->mod_time         = _htonl(ft->pfts.currentFileTime);
	oft->checksum         = _htonl(aim_oft_checksum_file(ft->pfts.tszCurrentFile));
	oft->recv_RFchecksum  = 0x0000FFFF;
	oft->RFchecksum       = 0x0000FFFF;
	oft->recv_checksum    = 0x0000FFFF;
	memcpy(oft->idstring, "Cool FileXfer", 13);
	oft->flags            = 0x20;
	oft->list_name_offset = 0x1c;
	oft->list_size_offset = 0x11;
	oft->encoding         = _htons(astr.isUnicode() ? 2 : 0);
	memcpy(oft->filename, astr.getBuf(), astr.getTermSize());

	if (!ft->requester || ft->pfts.currentFileNumber)
		memcpy(oft->icbm_cookie, ft->icbm_cookie, 8);

	return Netlib_Send(ft->hConn, (char*)oft, len, 0) > 0;
}

void CAimProto::report_file_error(TCHAR *fname)
{
	TCHAR errmsg[512];
	TCHAR* error = mir_a2t(_strerror(NULL));
	mir_sntprintf(errmsg, SIZEOF(errmsg), TranslateT("Failed to open file: %s : %s"), fname, error);
	mir_free(error);
	ShowPopup((char*)errmsg, ERROR_POPUP | TCHAR_POPUP);
}

bool setup_next_file_send(file_transfer *ft)
{
	TCHAR *file;
	struct _stati64 statbuf;
	for (;;)
	{
		file = ft->pfts.ptszFiles[ft->cf];
		if (file == NULL) return false;

		if (_tstati64(file, &statbuf) == 0 && (statbuf.st_mode & _S_IFDIR) == 0) 
			break;

		++ft->cf;
	}

	ft->pfts.tszCurrentFile = file;
	ft->pfts.currentFileSize = statbuf.st_size;
	ft->pfts.currentFileTime = statbuf.st_mtime;
	ft->pfts.currentFileProgress = 0;

	char* fnamea;
	char* fname = mir_utf8encodeT(file);
	if (ft->pfts.totalFiles > 1 && ft->file[0])
	{
		size_t dlen = strlen(ft->file);
		if (strncmp(fname, ft->file, dlen) == 0 && fname[dlen] == '\\')
		{
			fnamea = &fname[dlen+1];
			for (char *p = fnamea; *p; ++p) { if (*p == '\\') *p = 1; }
		}
		else
			fnamea = get_fname(fname);
	}
	else
		fnamea = get_fname(fname);

	send_init_oft2(ft, fnamea);

	mir_free(fname);
	return true;
}

int CAimProto::sending_file(file_transfer *ft, HANDLE hServerPacketRecver, NETLIBPACKETRECVER &packetRecv)
{
	LOG("P2P: Entered file sending thread.");

	bool failed = true;
	bool failed_conn = false;

	if (!setup_next_file_send(ft)) return 2;

	LOG("Sent file information to buddy.");
	//start listen for packets stuff

	for (;;)
	{
		int recvResult = packetRecv.bytesAvailable - packetRecv.bytesUsed;
		if (recvResult <= 0)
			recvResult = CallService(MS_NETLIB_GETMOREPACKETS, (WPARAM)hServerPacketRecver, (LPARAM)&packetRecv);
		if (recvResult == 0)
		{
			LOG("P2P: File transfer connection Error: 0");
			break;
		}
		if (recvResult == SOCKET_ERROR)
		{
			failed_conn = true;
			LOG("P2P: File transfer connection Error: -1");
			break;
		}
		if (recvResult > 0)
		{	
			if (recvResult < 0x100) continue;
		   
			oft2* recv_ft = (oft2*)&packetRecv.buffer[packetRecv.bytesUsed];

			unsigned short pkt_len = _htons(recv_ft->length);
			if (recvResult < pkt_len) continue;

			packetRecv.bytesUsed += pkt_len;
			unsigned short type = _htons(recv_ft->type);
			if (type == 0x0202 || type == 0x0207)
			{
				LOG("P2P: Buddy Accepts our file transfer.");

				int fid = _topen(ft->pfts.tszCurrentFile, _O_RDONLY | _O_BINARY, _S_IREAD);
				if (fid < 0)
				{
					report_file_error(ft->pfts.tszCurrentFile);
					return 2;
				}

				if (ft->pfts.currentFileProgress) _lseeki64(fid, ft->pfts.currentFileProgress, SEEK_SET);

				NETLIBSELECT tSelect = {0};
				tSelect.cbSize = sizeof(tSelect);
				tSelect.hReadConns[0] = ft->hConn;

				sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts);

				clock_t lNotify = clock();
				for (;;)
				{
					char buffer[4096];
					int bytes = _read(fid, buffer, sizeof(buffer));
					if (bytes <= 0) break;

					if (Netlib_Send(ft->hConn, buffer, bytes, MSG_NODUMP) <= 0) break;
					ft->pfts.currentFileProgress += bytes;
					ft->pfts.totalProgress += bytes;
					
					if (clock() >= lNotify)
					{
						sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts);
						if (CallService(MS_NETLIB_SELECT, 0, (LPARAM)&tSelect)) break;

						lNotify = clock() + 500;
					}
				}
				sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts);
				LOG("P2P: Finished sending file bytes.");
				_close(fid);
			}
			else if (type == 0x0204)
			{
				// Handle file skip case
				if (ft->pfts.currentFileProgress == 0)
				{
					ft->pfts.totalProgress += ft->pfts.currentFileSize;
				}

				LOG("P2P: Buddy says they got the file successfully");
				if ((ft->pfts.currentFileNumber + 1) < ft->pfts.totalFiles)
				{
					sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
					++ft->pfts.currentFileNumber; ++ft->cf;

					if (!setup_next_file_send(ft))
					{
						report_file_error(ft->pfts.tszCurrentFile);
						return 2;
					}
				}
				else
				{
					failed = _htonl(recv_ft->recv_bytes) != ft->pfts.currentFileSize;
					break;
				}
			}
			else if (type == 0x0205)
			{
				oft2* recv_ft = (oft2*)packetRecv.buffer;
				recv_ft->type = _htons(0x0106);
				
				ft->pfts.currentFileProgress = _htonl(recv_ft->recv_bytes);
				if (aim_oft_checksum_file(ft->pfts.tszCurrentFile, ft->pfts.currentFileProgress) != _htonl(recv_ft->recv_checksum))
				{
					ft->pfts.currentFileProgress = 0;
					recv_ft->recv_bytes = 0;
				}

				LOG("P2P: Buddy wants us to start sending at a specified file point. (%I64u)", ft->pfts.currentFileProgress);
				if (Netlib_Send(ft->hConn, (char*)recv_ft, _htons(recv_ft->length), 0) <= 0) break;

				ft->pfts.totalProgress += ft->pfts.currentFileProgress;
				sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts);
			}
		}
	}

	ft->success = !failed;
	return failed ? (failed_conn ? 1 : 2) : 0;
}

int CAimProto::receiving_file(file_transfer *ft, HANDLE hServerPacketRecver, NETLIBPACKETRECVER &packetRecv)
{
	LOG("P2P: Entered file receiving thread.");
	bool failed = true;
	bool failed_conn = false;
	bool accepted_file = false;
	int fid = -1;

	oft2 *oft = NULL;

	ft->pfts.tszWorkingDir = mir_utf8decodeT(ft->file);

	//start listen for packets stuff
	for (;;)
	{
		int recvResult = packetRecv.bytesAvailable - packetRecv.bytesUsed;
		if (recvResult <= 0)
			recvResult = CallService(MS_NETLIB_GETMOREPACKETS, (WPARAM)hServerPacketRecver, (LPARAM)&packetRecv);
		if (recvResult == 0)
		{
			LOG("P2P: File transfer connection Error: 0");
			break;
		}
		if (recvResult == SOCKET_ERROR)
		{
			failed_conn = true;
			LOG("P2P: File transfer connection Error: -1");
			break;
		}
		if (recvResult > 0)
		{	
			if (!accepted_file)
			{
				if (recvResult < 0x100) continue;
			   
				oft2* recv_ft = (oft2*)&packetRecv.buffer[packetRecv.bytesUsed];
				unsigned short pkt_len = _htons(recv_ft->length);

				if (recvResult < pkt_len) continue;
				packetRecv.bytesUsed += pkt_len;

				unsigned short type = _htons(recv_ft->type);
				if (type == 0x0101)
				{
					LOG("P2P: Buddy Ready to begin transfer.");
					oft = (oft2*)mir_realloc(oft, pkt_len);
					memcpy(oft, recv_ft, pkt_len);
					memcpy(oft->icbm_cookie, ft->icbm_cookie, 8);

					int buflen = pkt_len - 0x100 + 64;
					char *buf = (char*)mir_calloc(buflen + 2);
					unsigned short enc;

					ft->pfts.currentFileSize = _htonl(recv_ft->size);
					ft->pfts.totalBytes = _htonl(recv_ft->total_size);
					ft->pfts.currentFileTime = _htonl(recv_ft->mod_time);
					memcpy(buf, recv_ft->filename, buflen);
					enc = _htons(recv_ft->encoding);

					TCHAR *name;
					if (enc == 2)
					{
						wchar_t* wbuf = (wchar_t*)buf;
						wcs_htons(wbuf);
						for (wchar_t *p = wbuf; *p; ++p) { if (*p == 1) *p = '\\'; }
						name = mir_u2t(wbuf);
					}
					else
					{
						for (char *p = buf; *p; ++p) { if (*p == 1) *p = '\\'; }
						name = mir_a2t(buf);
					}

					mir_free(buf);

					TCHAR fname[256];
					mir_sntprintf(fname, SIZEOF(fname), _T("%s%s"), ft->pfts.tszWorkingDir, name);
					mir_free(name);
					mir_free(ft->pfts.tszCurrentFile);
					ft->pfts.tszCurrentFile = mir_tstrdup(fname);

					ResetEvent(ft->hResumeEvent);
					if (sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&ft->pfts))
						WaitForSingleObject(ft->hResumeEvent, INFINITE);

					if (ft->pfts.tszCurrentFile)
					{
						TCHAR* dir = get_dir(ft->pfts.tszCurrentFile);
						CallService(MS_UTILS_CREATEDIRTREET, 0, (LPARAM)dir);
						mir_free(dir);

						oft->type = _htons(ft->pfts.currentFileProgress ? 0x0205 : 0x0202);

						const int flag = ft->pfts.currentFileProgress ? 0 : _O_TRUNC;
						fid = _topen(ft->pfts.tszCurrentFile, _O_CREAT | _O_WRONLY | _O_BINARY | flag, _S_IREAD | _S_IWRITE);

						if (fid < 0)	
						{
							report_file_error(fname);
							break;
						}

						accepted_file = ft->pfts.currentFileProgress == 0;

						if (ft->pfts.currentFileProgress)
						{
							bool the_same;
							oft->recv_bytes = _htonl(ft->pfts.currentFileProgress);
							oft->recv_checksum = _htonl(aim_oft_checksum_file(ft->pfts.tszCurrentFile));
							the_same = oft->size == oft->recv_bytes && oft->checksum == oft->recv_checksum;
							if (the_same)
							{
								ft->pfts.totalProgress += ft->pfts.currentFileProgress;
								oft->type = _htons(0x0204);
								_close(fid);

								sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
								++ft->pfts.currentFileNumber;
								ft->pfts.currentFileProgress = 0;
							}
						}
					}
					else
					{
						oft->type = _htons(0x0204);

						sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
						++ft->pfts.currentFileNumber;
						ft->pfts.currentFileProgress = 0;
					}

					if (Netlib_Send(ft->hConn, (char*)oft, pkt_len, 0) == SOCKET_ERROR)
						break;

					if (ft->pfts.currentFileNumber >= ft->pfts.totalFiles && _htons(oft->type) == 0x0204)
					{
						failed = false;
						break;
					}
				}
				else if (type == 0x0106)
				{
					oft = (oft2*)mir_realloc(oft, pkt_len);
					memcpy(oft, recv_ft, pkt_len);

					ft->pfts.currentFileProgress = _htonl(oft->recv_bytes);
					ft->pfts.totalProgress += ft->pfts.currentFileProgress;

					_lseeki64(fid, ft->pfts.currentFileProgress, SEEK_SET);
					accepted_file = true;

					oft->type = _htons(0x0207);
					if (Netlib_Send(ft->hConn, (char*)oft, pkt_len, 0) == SOCKET_ERROR)
						break;
				}
				else
					break;
			}
			else
			{
				packetRecv.bytesUsed = packetRecv.bytesAvailable;
				_write(fid, packetRecv.buffer, packetRecv.bytesAvailable);
				ft->pfts.currentFileProgress += packetRecv.bytesAvailable;
				ft->pfts.totalProgress       += packetRecv.bytesAvailable;
				sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts);
				if (ft->pfts.currentFileSize == ft->pfts.currentFileProgress)
				{
					oft->type = _htons(0x0204);
					oft->recv_bytes = _htonl(ft->pfts.currentFileProgress);
					oft->recv_checksum = _htonl(aim_oft_checksum_file(ft->pfts.tszCurrentFile));

					LOG("P2P: We got the file successfully");
					Netlib_Send(ft->hConn, (char*)oft, _htons(oft->length), 0);
					if (_htons(oft->num_files_left) == 1)
					{
						failed = false;
						break;
					}
					else
					{
						accepted_file = false;
						_close(fid);

						sendBroadcast(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
						++ft->pfts.currentFileNumber;
						ft->pfts.currentFileProgress = 0;
					}
				}
			}
		}
	}

	if (accepted_file) _close(fid);
	mir_free(oft);

	ft->success = !failed;
	return failed ? (failed_conn ? 1 : 2) : 0;
}

void CAimProto::shutdown_file_transfers(void)
{
	for(int i=0; i<ft_list.getCount(); ++i)
	{
		file_transfer& ft = ft_list[i];
		if (ft.hConn)
			Netlib_Shutdown(ft.hConn);
	}
}

ft_list_type::ft_list_type() :  OBJLIST <file_transfer>(10) {};

file_transfer* ft_list_type::find_by_cookie(char* cookie, HANDLE hContact)
{
	for (int i = 0; i < getCount(); ++i)
	{
		file_transfer *ft = items[i];
		if (ft->hContact == hContact && memcmp(ft->icbm_cookie, cookie, 8) == 0)
			return ft;
	}
	return NULL;
}

file_transfer* ft_list_type::find_by_ip(unsigned long ip)
{
	for (int i = getCount(); i--; )
	{
		file_transfer *ft = items[i];
		if (ft->accepted && ft->requester && (ft->local_ip == ip || ft->verified_ip == ip))
			return ft;
	}
	return NULL;
}

file_transfer* ft_list_type::find_suitable(void)
{
	for (int i = getCount(); i--; )
	{
		file_transfer *ft = items[i];
		if (ft->accepted && ft->requester)
			return ft;
	}
	return NULL;
}


bool ft_list_type::find_by_ft(file_transfer *ft)
{
	for (int i = 0; i < getCount(); ++i)
	{
		if (items[i] == ft) return true;
	}
	return false;
}

void ft_list_type::remove_by_ft(file_transfer *ft)
{
	for (int i = 0; i < getCount(); ++i)
	{
		if (items[i] == ft)
		{
			remove(i);
			break;
		}
	}
}

file_transfer::file_transfer(HANDLE hCont, char* nick, char* cookie)  
{ 
	memset(this, 0, sizeof(*this)); 

	pfts.cbSize = sizeof(pfts);
	pfts.flags = PFTS_TCHAR;
	pfts.hContact = hCont;

	hContact = hCont;
	sn = mir_strdup(nick);

	if (cookie)
		memcpy(icbm_cookie, cookie, 8);
	else
		CallService(MS_UTILS_GETRANDOM, 8, (LPARAM)icbm_cookie);
	
	hResumeEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
}

file_transfer::~file_transfer() 
{ 
	mir_free(file); 
	mir_free(message); 
	mir_free(sn); 

	mir_free(pfts.tszWorkingDir);
	if (!sending) mir_free(pfts.tszCurrentFile);

	if (success && pfts.ptszFiles)
	{
		for (int i = 0; pfts.ptszFiles[i]; i++)
			mir_free(pfts.ptszFiles[i]);

		mir_free(pfts.ptszFiles);
	}
	CloseHandle(hResumeEvent);
}
