// ---------------------------------------------------------------------------80
//                ICQ plugin for Miranda Instant Messenger
//                ________________________________________
//
// Copyright � 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede
// Copyright � 2001-2002 Jon Keating, Richard Hughes
// Copyright � 2002-2004 Martin �berg, Sam Kothari, Robert Rainwater
// Copyright � 2004-2009 Joe Kucera
// Copyright � 2012-2014 Miranda NG Team
//
// 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"

static void file_buildProtoFileTransferStatus(filetransfer* ft, PROTOFILETRANSFERSTATUS* pfts)
{
	memset(pfts, 0, sizeof(PROTOFILETRANSFERSTATUS));
	pfts->cbSize = sizeof(PROTOFILETRANSFERSTATUS);
	pfts->hContact = ft->hContact;
	pfts->flags = PFTS_UTF | (ft->sending ? PFTS_SENDING : PFTS_RECEIVING); /* Standard FT is Ansi only */
	if (ft->sending)
		pfts->pszFiles = ft->pszFiles;
	else
		pfts->pszFiles = NULL;  /* FIXME */
	pfts->totalFiles = ft->dwFileCount;
	pfts->currentFileNumber = ft->iCurrentFile;
	pfts->totalBytes = ft->dwTotalSize;
	pfts->totalProgress = ft->dwBytesDone;
	pfts->szWorkingDir = ft->szSavePath;
	pfts->szCurrentFile = ft->szThisFile;
	pfts->currentFileSize = ft->dwThisFileSize;
	pfts->currentFileTime = ft->dwThisFileDate;
	pfts->currentFileProgress = ft->dwFileBytesDone;
}


static void file_sendTransferSpeed(CIcqProto* ppro, directconnect* dc)
{
	icq_packet packet;
	directPacketInit(&packet, 5);
	packByte(&packet, PEER_FILE_SPEED);    /* Ident */
	packLEDWord(&packet, dc->ft->dwTransferSpeed);
	ppro->sendDirectPacket(dc, &packet);
}


static void file_sendNick(CIcqProto* ppro, directconnect* dc)
{
	ptrA tmp(ppro->getStringA("Nick"));
	char *szNick = NEWSTR_ALLOCA((tmp == NULL) ? "" : tmp);
	size_t nNickLen = mir_strlen(szNick);

	icq_packet packet;
	directPacketInit(&packet, 8 + nNickLen);
	packByte(&packet, PEER_FILE_INIT_ACK); /* Ident */
	packLEDWord(&packet, dc->ft->dwTransferSpeed);
	packLEWord(&packet, WORD(nNickLen + 1));
	packBuffer(&packet, (LPBYTE)szNick, nNickLen + 1);
	ppro->sendDirectPacket(dc, &packet);
}


static void file_sendNextFile(CIcqProto* ppro, directconnect* dc)
{
	struct _stati64 statbuf;
	char szThisSubDir[MAX_PATH];

	if (dc->ft->iCurrentFile >= (int)dc->ft->dwFileCount) {
		ppro->ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, dc->ft, 0);
		ppro->CloseDirectConnection(dc);
		dc->ft->hConnection = NULL;
		return;
	}

	dc->ft->szThisFile = dc->ft->pszFiles[dc->ft->iCurrentFile];
	if (FileStatUtf(dc->ft->szThisFile, &statbuf)) {
		ppro->icq_LogMessage(LOG_ERROR, LPGEN("Your file transfer has been aborted because one of the files that you selected to send is no longer readable from the disk. You may have deleted or moved it."));
		ppro->CloseDirectConnection(dc);
		dc->ft->hConnection = NULL;
		return;
	}

	char *pszThisFileName = FindFilePathContainer((LPCSTR*)dc->ft->pszFiles, dc->ft->iCurrentFile, szThisSubDir);

	if (statbuf.st_mode & _S_IFDIR)
		dc->ft->currentIsDir = 1;
	else {
		dc->ft->currentIsDir = 0;
		dc->ft->fileId = OpenFileUtf(dc->ft->szThisFile, _O_BINARY | _O_RDONLY, _S_IREAD);
		if (dc->ft->fileId == -1) {
			ppro->icq_LogMessage(LOG_ERROR, LPGEN("Your file transfer has been aborted because one of the files that you selected to send is no longer readable from the disk. You may have deleted or moved it."));
			ppro->CloseDirectConnection(dc);
			dc->ft->hConnection = NULL;
			return;
		}
	}

	dc->ft->dwThisFileSize = statbuf.st_size;
	dc->ft->dwThisFileDate = statbuf.st_mtime;
	dc->ft->dwFileBytesDone = 0;

	char *szThisFileNameAnsi = NULL, *szThisSubDirAnsi = NULL;
	if (!utf8_decode(pszThisFileName, &szThisFileNameAnsi))
		szThisFileNameAnsi = _strdup(pszThisFileName);	// Legacy fix
	if (!utf8_decode(szThisSubDir, &szThisSubDirAnsi))
		szThisSubDirAnsi = _strdup(szThisSubDir);		// Legacy fix
	
	size_t wThisFileNameLen = mir_strlen(szThisFileNameAnsi);
	size_t wThisSubDirLen = mir_strlen(szThisSubDirAnsi);

	icq_packet packet;
	directPacketInit(&packet, 20 + wThisFileNameLen + wThisSubDirLen);
	packByte(&packet, PEER_FILE_NEXTFILE); /* Ident */
	packByte(&packet, (BYTE)((statbuf.st_mode & _S_IFDIR) != 0)); // Is subdir
	packLEWord(&packet, WORD(wThisFileNameLen + 1));
	packBuffer(&packet, (LPBYTE)szThisFileNameAnsi, wThisFileNameLen + 1);
	packLEWord(&packet, WORD(wThisSubDirLen + 1));
	packBuffer(&packet, (LPBYTE)szThisSubDirAnsi, wThisSubDirLen + 1);
	packLEDWord(&packet, dc->ft->dwThisFileSize);
	packLEDWord(&packet, statbuf.st_mtime);
	packLEDWord(&packet, dc->ft->dwTransferSpeed);
	SAFE_FREE(&szThisFileNameAnsi);
	SAFE_FREE(&szThisSubDirAnsi);
	ppro->sendDirectPacket(dc, &packet);

	ppro->ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, dc->ft, 0);
}


static void file_sendResume(CIcqProto* ppro, directconnect* dc)
{
	icq_packet packet;
	directPacketInit(&packet, 17);
	packByte(&packet, PEER_FILE_RESUME);            /* Ident */
	packLEDWord(&packet, dc->ft->dwFileBytesDone);  /* file resume */
	packLEDWord(&packet, 0);                        /* unknown */
	packLEDWord(&packet, dc->ft->dwTransferSpeed);
	packLEDWord(&packet, dc->ft->iCurrentFile + 1); /* file number */
	ppro->sendDirectPacket(dc, &packet);
}


static void file_sendData(CIcqProto* ppro, directconnect* dc)
{
	BYTE buf[2048];
	int bytesRead = 0;

	if (!dc->ft->currentIsDir) {
		icq_packet packet;

		if (dc->ft->fileId == -1)
			return;
		bytesRead = _read(dc->ft->fileId, buf, sizeof(buf));
		if (bytesRead == -1)
			return;

		directPacketInit(&packet, 1 + bytesRead);
		packByte(&packet, PEER_FILE_DATA);   /* Ident */
		packBuffer(&packet, buf, bytesRead);
		ppro->sendDirectPacket(dc, &packet);
	}

	dc->ft->dwBytesDone += bytesRead;
	dc->ft->dwFileBytesDone += bytesRead;

	if (GetTickCount() > dc->ft->dwLastNotify + 500 || bytesRead == 0) {
		PROTOFILETRANSFERSTATUS pfts;

		file_buildProtoFileTransferStatus(dc->ft, &pfts);
		ppro->ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, dc->ft, (LPARAM)&pfts);

		dc->ft->dwLastNotify = GetTickCount();
	}

	if (bytesRead == 0) {
		if (!dc->ft->currentIsDir) _close(dc->ft->fileId);
		dc->ft->fileId = -1;
		dc->wantIdleTime = 0;
		dc->ft->iCurrentFile++;
		file_sendNextFile(ppro, dc);   /* this will close the socket if no more files */
	}
}


void CIcqProto::handleFileTransferIdle(directconnect* dc)
{
	file_sendData(this, dc);
}


void CIcqProto::icq_sendFileResume(filetransfer *ft, int action, const char *szFilename)
{
	if (ft->hConnection == NULL)
		return;

	directconnect *dc = FindFileTransferDC(ft);
	if (!dc)
		return; // something is broken...

	int openFlags;

	switch (action) {
	case FILERESUME_RESUME:
		openFlags = _O_BINARY | _O_WRONLY;
		break;

	case FILERESUME_OVERWRITE:
		openFlags = _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY;
		ft->dwFileBytesDone = 0;
		break;

	case FILERESUME_SKIP:
		openFlags = _O_BINARY | _O_WRONLY;
		ft->dwFileBytesDone = ft->dwThisFileSize;
		break;

	case FILERESUME_RENAME:
		openFlags = _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY;
		SAFE_FREE(&ft->szThisFile);
		ft->szThisFile = null_strdup(szFilename);
		ft->dwFileBytesDone = 0;
		break;

	default:
		openFlags = 0;
	}

	ft->fileId = OpenFileUtf(ft->szThisFile, openFlags, _S_IREAD | _S_IWRITE);
	if (ft->fileId == -1) {
		icq_LogMessage(LOG_ERROR, LPGEN("Your file receive has been aborted because Miranda could not open the destination file in order to write to it. You may be trying to save to a read-only folder."));
		NetLib_CloseConnection(&ft->hConnection, FALSE);
		return;
	}

	if (action == FILERESUME_RESUME)
		ft->dwFileBytesDone = _lseek(ft->fileId, 0, SEEK_END);
	else
		_lseek(ft->fileId, ft->dwFileBytesDone, SEEK_SET);

	ft->dwBytesDone += ft->dwFileBytesDone;

	file_sendResume(this, dc);

	ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
}


// small utility function
void NormalizeBackslash(char* path)
{
	size_t len = mir_strlen(path);
	if (len && path[len-1] != '\\')
		strcat(path, "\\");
}

/* a file transfer looks like this:
S: 0
R: 5
R: 1
S: 2
R: 3
S: 6 * many
(for more files, send 2, 3, 6*many)
*/

void CIcqProto::handleFileTransferPacket(directconnect* dc, PBYTE buf, size_t wLen)
{
	if (wLen < 1)
		return;

	NetLog_Direct("Handling file packet");

	switch (buf[0]) {
	case PEER_FILE_INIT:   /* first packet of a file transfer */
		if (dc->initialised)
			return;
		if (wLen < 19)
			return;
		buf += 5;  /* id, and unknown 0 */
		dc->type = DIRECTCONN_FILE;
		{
			DWORD dwFileCount;
			DWORD dwTotalSize;
			DWORD dwTransferSpeed;
			size_t wNickLength;
			int bAdded;

			unpackLEDWord(&buf, &dwFileCount);
			unpackLEDWord(&buf, &dwTotalSize);
			unpackLEDWord(&buf, &dwTransferSpeed);
			unpackLEWord(&buf, &wNickLength);

			dc->ft = FindExpectedFileRecv(dc->dwRemoteUin, dwTotalSize);
			if (dc->ft == NULL) {
				NetLog_Direct("Unexpected file receive");
				CloseDirectConnection(dc);
				return;
			}
			dc->ft->dwFileCount = dwFileCount;
			dc->ft->dwTransferSpeed = dwTransferSpeed;
			dc->ft->hContact = HContactFromUIN(dc->ft->dwUin, &bAdded);
			dc->ft->dwBytesDone = 0;
			dc->ft->iCurrentFile = -1;
			dc->ft->fileId = -1;
			dc->ft->hConnection = dc->hConnection;
			dc->ft->dwLastNotify = GetTickCount();

			dc->initialised = 1;

			file_sendTransferSpeed(this, dc);
			file_sendNick(this, dc);
		}
		ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, dc->ft, 0);
		break;

	case PEER_FILE_INIT_ACK:
		if (wLen < 8)
			return;
		buf++;
		unpackLEDWord(&buf, &dc->ft->dwTransferSpeed);
		/* followed by nick */
		file_sendNextFile(this, dc);
		break;

	case PEER_FILE_NEXTFILE:
		if (wLen < 20)
			return;
		buf++;  /* id */
		{
			size_t wThisFilenameLen, wSubdirLen;
			BYTE isDirectory;
			unpackByte(&buf, &isDirectory);
			unpackLEWord(&buf, &wThisFilenameLen);
			if (wLen < 19 + wThisFilenameLen)
				return;
			
			SAFE_FREE(&dc->ft->szThisFile);
			char *szAnsi = (char *)_alloca(wThisFilenameLen + 1);
			memcpy(szAnsi, buf, wThisFilenameLen);
			szAnsi[wThisFilenameLen] = '\0';
			dc->ft->szThisFile = ansi_to_utf8(szAnsi);
			buf += wThisFilenameLen;

			unpackLEWord(&buf, &wSubdirLen);
			if (wLen < 18 + wThisFilenameLen + wSubdirLen)
				return;
			SAFE_FREE(&dc->ft->szThisSubdir);
			szAnsi = (char *)_alloca(wSubdirLen + 1);
			memcpy(szAnsi, buf, wSubdirLen);
			szAnsi[wSubdirLen] = '\0';
			dc->ft->szThisSubdir = ansi_to_utf8(szAnsi);
			buf += wSubdirLen;

			unpackLEDWord(&buf, &dc->ft->dwThisFileSize);
			unpackLEDWord(&buf, &dc->ft->dwThisFileDate);
			unpackLEDWord(&buf, &dc->ft->dwTransferSpeed);

			/* no cheating with paths */
			if (!IsValidRelativePath(dc->ft->szThisFile) || !IsValidRelativePath(dc->ft->szThisSubdir)) {
				NetLog_Direct("Invalid path information");
				break;
			}

			char *szFullPath = (char*)SAFE_MALLOC(mir_strlen(dc->ft->szSavePath) + mir_strlen(dc->ft->szThisSubdir) + mir_strlen(dc->ft->szThisFile) + 3);
			mir_strcpy(szFullPath, dc->ft->szSavePath);
			NormalizeBackslash(szFullPath);
			strcat(szFullPath, dc->ft->szThisSubdir);
			NormalizeBackslash(szFullPath);
			//			_chdir(szFullPath); // set current dir - not very useful
			strcat(szFullPath, dc->ft->szThisFile);
			// we joined the full path to dest file
			SAFE_FREE(&dc->ft->szThisFile);
			dc->ft->szThisFile = szFullPath;

			dc->ft->dwFileBytesDone = 0;
			dc->ft->iCurrentFile++;

			if (isDirectory) {
				MakeDirUtf(dc->ft->szThisFile);
				dc->ft->fileId = -1;
			}
			else {
				/* file resume */
				PROTOFILETRANSFERSTATUS pfts = { 0 };

				file_buildProtoFileTransferStatus(dc->ft, &pfts);
				if (ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, dc->ft, (LPARAM)&pfts))
					break;   /* UI supports resume: it will call PS_FILERESUME */

				dc->ft->fileId = OpenFileUtf(dc->ft->szThisFile, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE);
				if (dc->ft->fileId == -1) {
					icq_LogMessage(LOG_ERROR, LPGEN("Your file receive has been aborted because Miranda could not open the destination file in order to write to it. You may be trying to save to a read-only folder."));
					CloseDirectConnection(dc);
					dc->ft->hConnection = NULL;
					break;
				}
			}
		}
		file_sendResume(this, dc);
		ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, dc->ft, 0);
		break;

	case PEER_FILE_RESUME:
		if (dc->ft->fileId == -1 && !dc->ft->currentIsDir)
			return;
		if (wLen < 13)
			return;
		if (wLen < 17)
			NetLog_Direct("Warning: Received short PEER_FILE_RESUME");
		buf++;
		{
			DWORD dwRestartFrom;

			unpackLEDWord(&buf, &dwRestartFrom);
			if (dwRestartFrom > dc->ft->dwThisFileSize)
				return;
			buf += 4;  /* unknown. 0 */
			unpackLEDWord(&buf, &dc->ft->dwTransferSpeed);
			buf += 4;  /* unknown. 1 */
			if (!dc->ft->currentIsDir)
				_lseek(dc->ft->fileId, dwRestartFrom, 0);
			dc->wantIdleTime = 1;
			dc->ft->dwBytesDone += dwRestartFrom;
			dc->ft->dwFileBytesDone += dwRestartFrom;
		}
		break;

	case PEER_FILE_SPEED:
		if (wLen < 5)
			return;
		buf++;
		unpackLEDWord(&buf, &dc->ft->dwTransferSpeed);
		dc->ft->dwLastNotify = GetTickCount();
		break;

	case PEER_FILE_DATA:
		if (!dc->ft->currentIsDir) {
			if (dc->ft->fileId == -1)
				break;
			buf++; wLen--;
			_write(dc->ft->fileId, buf, (unsigned)wLen);
		}
		else
			wLen = 0;
		dc->ft->dwBytesDone += (DWORD)wLen;
		dc->ft->dwFileBytesDone += (DWORD)wLen;
		if (GetTickCount() > dc->ft->dwLastNotify + 500 || wLen < 2048) {
			PROTOFILETRANSFERSTATUS pfts;

			file_buildProtoFileTransferStatus(dc->ft, &pfts);
			ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, dc->ft, (LPARAM)&pfts);
			dc->ft->dwLastNotify = GetTickCount();
		}
		if (wLen < 2048) {
			/* EOF */
			if (!dc->ft->currentIsDir)
				_close(dc->ft->fileId);
			dc->ft->fileId = -1;
			if ((DWORD)dc->ft->iCurrentFile == dc->ft->dwFileCount - 1) {
				dc->type = DIRECTCONN_CLOSING;     /* this guarantees that we won't accept any more data but that the sender is still free to closesocket() neatly */
				ProtoBroadcastAck(dc->ft->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, dc->ft, 0);
			}
		}
		break;

	default:
		NetLog_Direct("Unknown file transfer packet ignored.");
		break;
	}
}