// Windows
#include <Windows.h>

// CRT
#include <stdio.h>
#include <stdlib.h>

// FlowSshC
#include "FlowSshC.h"



enum ExitCodes
{
	Success,
	UsageError,
	SessionError, // including connection error 
	FatalError
};


struct Events
{
	HANDLE m_exitEvent;
	HANDLE m_readyEvent;
};


void ErrorHandler(void* handlerData, unsigned int flags, wchar_t const* desc)
{
	wprintf(L"Fatal error: %s\n", desc ? desc : L"<no error description>");
	// For the sake of simplicity, no cleanup is performed here.
	// The lack of cleanup may cause debugger to report memory leaks.
	exit(FatalError);
}


bool HostKeyHandler(void* handlerData, FlowSshC_PublicKey* publicKey)
{
	BSTR md5 = FlowSshC_PublicKey_GetMd5(publicKey);
	BSTR bb = FlowSshC_PublicKey_GetBubbleBabble(publicKey);
	BSTR sha256 = FlowSshC_PublicKey_GetSha256(publicKey);

	wprintf(L"Received the following host key:\n"
			L"MD5 Fingerprint: %s\n"
			L"Bubble-Babble: %s\n"
			L"SHA-256: %s\n",
			md5, bb, sha256);

	SysFreeString(md5);
	SysFreeString(bb);
	SysFreeString(sha256);

	wprintf(L"Accept the key (yes/no)? ");

	wchar_t input[10];
	if (_getws_s(input, 9))
		return _wcsicmp(input, L"yes") == 0;
	else
		return false;
}

void DisconnectHandler(void* handlerData, unsigned int reason, wchar_t const* desc)
{
	wprintf(L"%s", desc);

	SetEvent(((Events*) handlerData)->m_exitEvent);
}

void ConnectProgressHandler(void* handlerData, unsigned int taskState, unsigned int taskSpecificStep, wchar_t const* auxInfo)
{
	if (taskState == FlowSshC_TaskState_Completed)
	{
		SetEvent(((Events*) handlerData)->m_readyEvent);
	}
	else if (taskState == FlowSshC_TaskState_Failed)
	{
		if (taskSpecificStep == FlowSshC_ConnectStep_ConnectToProxy)
			wprintf(L"Connecting to proxy server failed: %s\n", auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_ConnectToSshServer)
			wprintf(L"Connecting to SSH server failed: %s\n", auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshVersionString)
			wprintf(L"SSH version string failed: %s\n", auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshKeyExchange)
			wprintf(L"SSH key exchange failed: %s\n", auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshUserAuth)
			wprintf(L"SSH authentication failed: %s\n", auxInfo ? auxInfo : L"(no additional info)");
		else
			wprintf(L"Connecting failed at unknown step: %s\n", auxInfo ? auxInfo : L"(no additional info)");

		SetEvent(((Events*) handlerData)->m_exitEvent);
	}
}


void ChannelCloseHandler(void* handlerData)
{
	wprintf(L"SFTP channel closed unexpectedly by the server.\n");

	SetEvent(((Events*) handlerData)->m_exitEvent);
}

void OpenChannelProgressHandler(void* handlerData, unsigned int taskState, unsigned int taskSpecificStep, wchar_t const* auxInfo)
{
	if (taskState == FlowSshC_TaskState_Completed)
	{
		SetEvent(((Events*) handlerData)->m_readyEvent);
	}
	else if (taskState == FlowSshC_TaskState_Failed)
	{
		if (taskSpecificStep == FlowSshC_ClientSftpChannelOpenStep_OpenRequest)
			wprintf(L"Opening SFTP channel failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ClientSftpChannelOpenStep_SftpRequest)
			wprintf(L"Requesting SFTP subsystem failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ClientSftpChannelOpenStep_InitPacket)
			wprintf(L"Initializing SFTP protocol failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else
			wprintf(L"Opening SFTP channel failed at unknown step: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
	
		SetEvent(((Events*) handlerData)->m_exitEvent);
	}
}


bool TransferHandler(void* handlerData, bool done, FlowSshC_TransferStat const* transferStat, FlowSshC_TransferErr const* transferErr)
{
	if (done)
	{
		wprintf(L"File transfer completed.\n");
		SetEvent(((Events*) handlerData)->m_readyEvent);
	}
	else if (transferErr)
	{
		if (transferErr->m_transferOp < 100) // codeless errors
			wprintf(L"File transfer failed: %s\n", transferErr->m_errMsg ? transferErr->m_errMsg : L"<no error message>");
		else if (transferErr->m_transferOp < 200) // SFTP errors
		{
			wchar_t const* action = L"Transfer";
			if (transferErr->m_transferOp == FlowSshC_TransferOp_MakeRemoteDir)
				action = L"Create remote directory";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_OpenRemoteFile)
				action = L"Opening remote file";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_ReadRemoteFile)
				action = L"Reading remote file";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_WriteRemoteFile)
				action = L"Writing remote file";
				
			wprintf(L"%s failed with SFTP error %u: %s\n", action, transferErr->m_errCode, transferErr->m_errMsg ? transferErr->m_errMsg : L"<no error message>");
		}
		else if (transferErr->m_transferOp < 300) // WinAPI errors
		{
			wchar_t const* action = L"Transfer";
			if (transferErr->m_transferOp == FlowSshC_TransferOp_MakeLocalDir)
				action = L"Create local directory";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_OpenLocalFile)
				action = L"Opening local file";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_ReadLocalFile)
				action = L"Reading local file";
			else if (transferErr->m_transferOp == FlowSshC_TransferOp_WriteLocalFile)
				action = L"Writing local file";
				
			wprintf(L"%s failed with Windows error %u: %s\n", action, transferErr->m_errCode, transferErr->m_errMsg ? transferErr->m_errMsg : L"<no error message>");
		}
		else // FlowSshC_ResumeErrCode
		{
			wprintf(L"Resuming file failed: %s\n", transferErr->m_errMsg ? transferErr->m_errMsg : L"<no error message>");
		}

		SetEvent(((Events*) handlerData)->m_exitEvent);
	}

	return true;
}


void DisconnectProgressHandler(void* handlerData, unsigned int taskState, unsigned int taskSpecificStep, wchar_t const* auxInfo)
{
	if (taskState == FlowSshC_TaskState_Completed || taskState == FlowSshC_TaskState_Failed)
		SetEvent((HANDLE) handlerData);
}


int wmain(int argc, wchar_t const* argv[])
{
	FlowSshC_Initialize(ErrorHandler, 0);

	// For use in deployed applications where Bitvise SSH Client might not be installed, 
	// provide an activation code using SetActCode to ensure that FlowSsh does not  
	// display an evaluation dialog. On computers with Bitvise SSH Client, use of 
	// FlowSsh is permitted under the same terms as the Client; in this case, there 
	// will be no evaluation dialog, and SetActCode does not have to be called.
	//
	//FlowSshC_SetActCode(L"Enter Your Activation Code Here");

	FlowSshC_Client* client = FlowSshC_Client_Create();
	// CHANGE APPLICATION NAME IN PRODUCTION CODE!
	FlowSshC_Client_SetAppName(client, L"FlowSshC_Sftp 1.0");

	// Process command-line parameters

	bool syntaxError = false;

	bool download;
	wchar_t const* localPath = 0;
	wchar_t const* remotePath = 0;

	for (int i = 1; i < argc && !syntaxError; ++i)
	{
		if (wcsncmp(argv[i], L"-l=", 3) == 0)
			FlowSshC_Client_SetUserName(client, argv[i] + 3);
		else if (wcsncmp(argv[i], L"-pw=", 4) == 0)
			FlowSshC_Client_SetPassword(client, argv[i] + 4);
		else if (wcsncmp(argv[i], L"-P=", 3) == 0)
		{
			unsigned int port;
			if (swscanf_s(argv[i] + 3, L"%u", &port))
				FlowSshC_Client_SetPort(client, port);
		}
		else 
		{
			wchar_t const* str = argv[i];
			
			wchar_t const* at = wcschr(str, L'@');
			if (at)
			{
				BSTR userName = SysAllocStringLen(str, (UINT) (at - str));
				FlowSshC_Client_SetUserName(client, userName);
				SysFreeString(userName);

				str = at + 1;
			}

			wchar_t const* colon = wcschr(str, L':');
			if (at && !colon)
				syntaxError = true;
			else
			{
				// Note that absolute Windows paths have colon at 2nd position.
				if (at || (colon && colon != (str + 1))) 
				{
					BSTR host = SysAllocStringLen(str, (UINT) (colon - str));
					FlowSshC_Client_SetHost(client, host);
					SysFreeString(host);

					str = colon + 1;
					remotePath = str;
					if (!localPath)
						download = true;
				}
				else
				{
					localPath = str;
					if (!remotePath)
						download = false;
				}
			}
		}
	}

	int exitCode = Success;

	if (syntaxError || !localPath || !remotePath)
	{
		wprintf(L"FlowSshC sample SFTP client.\n"
				L"\n"
				L"Usage: FlowSshC_Sftp [Options] [User@]Host:SourceFile TargetFile\n"
				L"       FlowSshC_Sftp [Options] SourceFile [User@]Host:TargetFile\n"
				L"\n"
				L"Options:\n"
				L"       -l=user\n"
				L"       -pw=password\n"
				L"       -P=port\n");
			
		exitCode = UsageError;
	}
	else
	{
		// Create events

		Events events;
		events.m_exitEvent = CreateEvent(0, true, false, 0);
		events.m_readyEvent = CreateEvent(0, false, false, 0);

		if (!events.m_exitEvent || !events.m_readyEvent)
		{
			wprintf(L"CreateEvent() failed with Windows error %u.\n", GetLastError());
			exitCode = FatalError;
		}
		else
		{
			// Connect the client

			FlowSshC_Client_SetHostKeyHandler(client, HostKeyHandler, 0);
			FlowSshC_Client_SetDisconnectHandler(client, DisconnectHandler, &events);
			FlowSshC_Client_Connect(client, ConnectProgressHandler, &events);

			HANDLE handles[2] = { events.m_exitEvent, events.m_readyEvent };
			DWORD dw = WaitForMultipleObjects(2, handles, false, INFINITE);
			if (dw == WAIT_FAILED)
			{
				wprintf(L"WaitForMultipleObjects() failed with Windows error %u.\n", GetLastError());
				exitCode = FatalError;
			}
			else if (dw == WAIT_OBJECT_0)
				exitCode = SessionError;
			else
			{
				// Open SFTP channel

				FlowSshC_ClientSftpChannel* channel = FlowSshC_ClientSftpChannel_Create(client);
				FlowSshC_ClientSftpChannel_SetCloseHandler(channel, ChannelCloseHandler, &events);
				FlowSshC_ClientSftpChannel_Open(channel, OpenChannelProgressHandler, &events);
				
				dw = WaitForMultipleObjects(2, handles, false, INFINITE);
				if (dw == WAIT_FAILED)
				{
					wprintf(L"WaitForMultipleObjects() failed with Windows error %u.\n", GetLastError());
					exitCode = FatalError;
				}
				else if (dw == WAIT_OBJECT_0)
					exitCode = SessionError;
				else
				{
					// Transfer file

					if (download)
					{
						wprintf(L"Downloading '%s' to '%s'\n", remotePath, localPath);
						FlowSshC_ClientSftpChannel_Download(channel, remotePath, localPath, FlowSshC_TransferFlags_Binary, TransferHandler, &events);
					}
					else
					{
						wprintf(L"Uploading '%s' to '%s'\n", localPath, remotePath);
						FlowSshC_ClientSftpChannel_Upload(channel, localPath, remotePath, FlowSshC_TransferFlags_Binary, TransferHandler, &events);
					}
				
					dw = WaitForMultipleObjects(2, handles, false, INFINITE);
					if (dw == WAIT_FAILED)
					{
						wprintf(L"WaitForMultipleObjects() failed with Windows error %u.\n", GetLastError());
						exitCode = FatalError;
					}
					else if (dw == WAIT_OBJECT_0)
						exitCode = SessionError;
				}

				FlowSshC_ClientSftpChannel_SetCloseHandler(channel, 0, 0);
				FlowSshC_ClientSftpChannel_Release(channel);
			}
		}

		FlowSshC_Shutdown();

		if (events.m_exitEvent)
			CloseHandle(events.m_exitEvent);
		if (events.m_readyEvent)
			CloseHandle(events.m_readyEvent);
	}

	FlowSshC_Client_Release(client);
	return exitCode;
}