#include "FileZilla.h"
#include "asynchostresolver.h"
#include "ControlSocket.h"
#include "ftpcontrolsocket.h"
#include "sftpcontrolsocket.h"
#include "directorycache.h"
#include "logging_private.h"
#include "httpcontrolsocket.h"
#include "ratelimiter.h"

class wxFzEngineEvent : public wxEvent
{
public:
	wxFzEngineEvent(int id, enum EngineNotificationType eventType, int data = 0);
	virtual wxEvent *Clone() const;

	enum EngineNotificationType m_eventType;
	int data;
};

extern const wxEventType fzEVT_ENGINE_NOTIFICATION;
typedef void (wxEvtHandler::*fzEngineEventFunction)(wxFzEngineEvent&);

#define EVT_FZ_ENGINE_NOTIFICATION(id, fn) \
    DECLARE_EVENT_TABLE_ENTRY( \
        fzEVT_ENGINE_NOTIFICATION, id, -1, \
        (wxObjectEventFunction)(wxEventFunction) wxStaticCastEvent( fzEngineEventFunction, &fn ), \
        (wxObject *) NULL \
    ),

std::list<CFileZillaEnginePrivate*> CFileZillaEnginePrivate::m_engineList;
int CFileZillaEnginePrivate::m_activeStatusSend = 0;
int CFileZillaEnginePrivate::m_activeStatusRecv = 0;
std::list<CFileZillaEnginePrivate::t_failedLogins> CFileZillaEnginePrivate::m_failedLogins;

DEFINE_EVENT_TYPE(fzEVT_ENGINE_NOTIFICATION);

wxFzEngineEvent::wxFzEngineEvent(int id, enum EngineNotificationType eventType, int data /*=0*/) : wxEvent(id, fzEVT_ENGINE_NOTIFICATION)
{
	m_eventType = eventType;
	wxFzEngineEvent::data = data;
}

wxEvent *wxFzEngineEvent::Clone() const
{
	return new wxFzEngineEvent(*this);
}


BEGIN_EVENT_TABLE(CFileZillaEnginePrivate, wxEvtHandler)
	EVT_FZ_ENGINE_NOTIFICATION(wxID_ANY, CFileZillaEnginePrivate::OnEngineEvent)
	EVT_FZ_ASYNCHOSTRESOLVE(wxID_ANY, CFileZillaEnginePrivate::OnAsyncHostResolver)
	EVT_TIMER(wxID_ANY, CFileZillaEnginePrivate::OnTimer)
END_EVENT_TABLE();

CFileZillaEnginePrivate::CFileZillaEnginePrivate()
	: m_retryTimer(this)
{
	m_pRateLimiter = 0;
	m_maySendNotificationEvent = true;
	m_pEventHandler = 0;
	m_pControlSocket = 0;
	m_pCurrentCommand = 0;
	m_bIsInCommand = false;
	m_nControlSocketError = 0;
	m_asyncRequestCounter = 0;
	m_engineList.push_back(this);
	m_retryCount = 0;

	m_pLogging = new CLogging(this);
}

CFileZillaEnginePrivate::~CFileZillaEnginePrivate()
{
	delete m_pControlSocket;
	delete m_pCurrentCommand;

	// Delete notification list
	for (std::list<CNotification *>::iterator iter = m_NotificationList.begin(); iter != m_NotificationList.end(); iter++)
		delete *iter;

	for (std::list<CAsyncHostResolver *>::iterator iter = m_HostResolverThreads.begin(); iter != m_HostResolverThreads.end(); iter++)
	{
		CAsyncHostResolver* pResolver = *iter;
		pResolver->SetObsolete();
		pResolver->Wait();
		delete pResolver;					
	}

	// Remove ourself from the engine list
	for (std::list<CFileZillaEnginePrivate*>::iterator iter = m_engineList.begin(); iter != m_engineList.end(); iter++)
		if (*iter == this)
		{
			m_engineList.erase(iter);
			break;
		}

	delete m_pLogging;

	if (m_pRateLimiter)
		m_pRateLimiter->Free();
}

bool CFileZillaEnginePrivate::SendEvent(enum EngineNotificationType eventType, int data /*=0*/)
{
	wxFzEngineEvent evt(wxID_ANY, eventType, data);
	wxPostEvent(this, evt);
	return true;
}

void CFileZillaEnginePrivate::OnEngineEvent(wxFzEngineEvent &event)
{
	switch (event.m_eventType)
	{
	case engineCancel:
		if (!IsBusy())
			break;

		m_pControlSocket->Cancel();
		break;
	case engineTransferEnd:
		if (m_pControlSocket)
			m_pControlSocket->TransferEnd();
	default:
		break;
	}
}

void CFileZillaEnginePrivate::OnAsyncHostResolver(fzAsyncHostResolveEvent& event)
{
	if (m_HostResolverThreads.empty())
		return;
	CAsyncHostResolver *pResolver = m_HostResolverThreads.front();
	m_HostResolverThreads.pop_front();

	std::list<CAsyncHostResolver *> remaining;
	for (std::list<CAsyncHostResolver *>::iterator iter = m_HostResolverThreads.begin(); iter != m_HostResolverThreads.end(); iter++)
	{
		CAsyncHostResolver* pResolver = *iter;
		pResolver->SetObsolete();
		if (!pResolver->Done())
			remaining.push_back(pResolver);
		else
		{
			pResolver->Wait();
			delete pResolver;					
		}
	}
	m_HostResolverThreads.clear();
	m_HostResolverThreads = remaining;

	if (!pResolver->Done())
		m_HostResolverThreads.push_front(pResolver);
	else
	{
		if (!pResolver->Obsolete())
			m_pControlSocket->ContinueConnect(pResolver->Successful() ? &pResolver->m_Address : 0);
		pResolver->Wait();
		delete pResolver;
	}
}

bool CFileZillaEnginePrivate::IsBusy() const
{
	return m_pCurrentCommand != 0;
}

bool CFileZillaEnginePrivate::IsConnected() const
{
	if (!m_pControlSocket)
		return false;

	return m_pControlSocket->Connected();
}

const CCommand *CFileZillaEnginePrivate::GetCurrentCommand() const
{
	return m_pCurrentCommand;
}

enum Command CFileZillaEnginePrivate::GetCurrentCommandId() const
{
	if (!m_pCurrentCommand)
		return cmd_none;

	else
		return GetCurrentCommand()->GetId();
}

void CFileZillaEnginePrivate::AddNotification(CNotification *pNotification)
{
	m_lock.Enter();
	m_NotificationList.push_back(pNotification);

	if (m_maySendNotificationEvent && m_pEventHandler)
	{
		m_maySendNotificationEvent = false;
		m_lock.Leave();
		wxFzEvent evt(wxID_ANY);
		evt.SetEventObject(this);
		wxPostEvent(m_pEventHandler, evt);
	}
	else
		m_lock.Leave();
}

int CFileZillaEnginePrivate::ResetOperation(int nErrorCode)
{
	if (nErrorCode & FZ_REPLY_DISCONNECTED)
		m_lastListDir.Clear();

	if (m_pCurrentCommand)
	{
		if ((nErrorCode & FZ_REPLY_NOTSUPPORTED) == FZ_REPLY_NOTSUPPORTED)
		{
			wxASSERT(m_bIsInCommand);
			m_pLogging->LogMessage(Error, _("Command not supported by this protocol"));
		}

		if (m_pCurrentCommand->GetId() == cmd_connect)
		{
			if (!(nErrorCode & ~(FZ_REPLY_ERROR | FZ_REPLY_DISCONNECTED | FZ_REPLY_TIMEOUT)) && 
				nErrorCode & (FZ_REPLY_ERROR | FZ_REPLY_DISCONNECTED) &&
				m_retryCount < m_pOptions->GetOptionVal(OPTION_RECONNECTCOUNT))
			{
				m_retryCount++;

				const CConnectCommand *pConnectCommand = (CConnectCommand *)m_pCurrentCommand;

				RegisterFailedLoginAttempt(pConnectCommand->GetServer());
				unsigned int delay = GetRemainingReconnectDelay(pConnectCommand->GetServer());
				if (!delay)
					delay = 1;
				m_pLogging->LogMessage(Status, _("Waiting to retry..."));
				m_retryTimer.Start(delay, true);
				return FZ_REPLY_WOULDBLOCK;
			}
		}

		if (!m_bIsInCommand)
		{
			COperationNotification *notification = new COperationNotification();
			notification->nReplyCode = nErrorCode;
			notification->commandId = m_pCurrentCommand->GetId();
			AddNotification(notification);
		}
		else
			m_nControlSocketError |= nErrorCode;

		delete m_pCurrentCommand;
		m_pCurrentCommand = 0;
	}
	else if (nErrorCode & FZ_REPLY_DISCONNECTED)
	{
		wxASSERT(!m_bIsInCommand);
		
		COperationNotification *notification = new COperationNotification();
		notification->nReplyCode = nErrorCode;
		notification->commandId = cmd_none;
		AddNotification(notification);
	}

	if (!m_HostResolverThreads.empty())
		m_HostResolverThreads.front()->SetObsolete();

	return nErrorCode;
}

void CFileZillaEnginePrivate::SetActive(bool recv)
{
	if (m_pControlSocket)
		m_pControlSocket->SetAlive();

	m_lock.Enter();
	if (recv)
	{
		if (!m_activeStatusRecv)
		{
			m_activeStatusRecv = 2;
			m_lock.Leave();
			AddNotification(new CActiveNotification(true));
			return;
		}
	}
	else
	{
		if (!m_activeStatusSend)
		{
			m_activeStatusSend = 2;
			m_lock.Leave();
			AddNotification(new CActiveNotification(false));
			return;
		}
	}
	m_lock.Leave();
}

unsigned int CFileZillaEnginePrivate::GetNextAsyncRequestNumber()
{
	wxCriticalSectionLocker lock(m_lock);
	return ++m_asyncRequestCounter;
}

void CFileZillaEnginePrivate::AddNewAsyncHostResolver(CAsyncHostResolver* pResolver)
{
	wxASSERT(pResolver);

	if (!m_HostResolverThreads.empty())
		m_HostResolverThreads.front()->SetObsolete();
	
	m_HostResolverThreads.push_front(pResolver);
}

// Command handlers
int CFileZillaEnginePrivate::Connect(const CConnectCommand &command)
{
	if (IsConnected())
		return FZ_REPLY_ALREADYCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	m_retryCount = 0;
	m_pCurrentCommand = command.Clone();

	if (command.GetServer().GetPort() != CServer::GetDefaultPort(command.GetServer().GetProtocol()))
	{
		ServerProtocol protocol = CServer::GetProtocolFromPort(command.GetServer().GetPort());
		if (protocol != UNKNOWN && protocol != command.GetServer().GetProtocol())
			m_pLogging->LogMessage(Status, _("Selected port usually in use by a different protocol."));
	}
	
	return ContinueConnect();
}

int CFileZillaEnginePrivate::Disconnect(const CDisconnectCommand &command)
{
	if (!IsConnected())
		return FZ_REPLY_OK;

	m_pCurrentCommand = command.Clone();
	int res = m_pControlSocket->Disconnect();
	if (res == FZ_REPLY_OK)
	{
		delete m_pControlSocket;
		m_pControlSocket = 0;
	}

	return res;
}

int CFileZillaEnginePrivate::Cancel(const CCancelCommand &command)
{
	if (!IsBusy())
		return FZ_REPLY_OK;

	if (m_retryTimer.IsRunning())
	{
		wxASSERT(m_pCurrentCommand && m_pCurrentCommand->GetId() == cmd_connect);

		delete m_pControlSocket;
		m_pControlSocket = 0;
		
		delete m_pCurrentCommand;
		m_pCurrentCommand = 0;

		m_retryTimer.Stop();

		m_pLogging->LogMessage(::Error, _("Connection attempt interrupted by user"));
		COperationNotification *notification = new COperationNotification();
		notification->nReplyCode = FZ_REPLY_DISCONNECTED|FZ_REPLY_CANCELED;
		notification->commandId = cmd_connect;
		AddNotification(notification);

		return FZ_REPLY_WOULDBLOCK;
	}

	SendEvent(engineCancel);

	return FZ_REPLY_WOULDBLOCK;
}

int CFileZillaEnginePrivate::List(const CListCommand &command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	bool refresh = command.Refresh();
	if (!command.Refresh() && !command.GetPath().IsEmpty())
	{
		const CServer* pServer = m_pControlSocket->GetCurrentServer();
		if (pServer)
		{
			CDirectoryListing *pListing = new CDirectoryListing;
			CDirectoryCache cache;
			bool found = cache.Lookup(*pListing, *pServer, command.GetPath(), command.GetSubDir(), true);
			if (found)
			{
				
				if (pListing->m_hasUnsureEntries)
					refresh = true;
				else
				{
					m_lastListDir = pListing->path;
					m_lastListTime = wxDateTime::Now();
					CDirectoryListingNotification *pNotification = new CDirectoryListingNotification(pListing->path);
					AddNotification(pNotification);
					delete pListing;
					return FZ_REPLY_OK;
				}
			}
			delete pListing;
		}
	}
	if (IsBusy())
		return FZ_REPLY_BUSY;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->List(command.GetPath(), command.GetSubDir(), refresh);
}

int CFileZillaEnginePrivate::FileTransfer(const CFileTransferCommand &command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->FileTransfer(command.GetLocalFile(), command.GetRemotePath(), command.GetRemoteFile(), command.Download(), command.GetTransferSettings());
}

int CFileZillaEnginePrivate::RawCommand(const CRawCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetCommand() == _T(""))
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->RawCommand(command.GetCommand());
}

int CFileZillaEnginePrivate::Delete(const CDeleteCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetPath().IsEmpty() ||
		command.GetFile() == _T(""))
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->Delete(command.GetPath(), command.GetFile());
}

int CFileZillaEnginePrivate::RemoveDir(const CRemoveDirCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetPath().IsEmpty() ||
		command.GetSubDir() == _T(""))
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->RemoveDir(command.GetPath(), command.GetSubDir());
}

int CFileZillaEnginePrivate::Mkdir(const CMkdirCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetPath().IsEmpty() || !command.GetPath().HasParent())
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->Mkdir(command.GetPath());
}

int CFileZillaEnginePrivate::Rename(const CRenameCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetFromPath().IsEmpty() || command.GetToPath().IsEmpty() ||
		command.GetFromFile() == _T("") || command.GetToFile() == _T(""))
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->Rename(command);
}

int CFileZillaEnginePrivate::Chmod(const CChmodCommand& command)
{
	if (!IsConnected())
		return FZ_REPLY_NOTCONNECTED;

	if (IsBusy())
		return FZ_REPLY_BUSY;

	if (command.GetPath().IsEmpty() || command.GetFile().IsEmpty() ||
		command.GetPermission() == _T(""))
		return FZ_REPLY_SYNTAXERROR;

	m_pCurrentCommand = command.Clone();
	return m_pControlSocket->Chmod(command);
}

void CFileZillaEnginePrivate::SendDirectoryListingNotification(const CServerPath& path, bool onList, bool modified, bool failed)
{
	wxASSERT(m_pControlSocket);
	wxASSERT(onList || modified);
	
	const CServer* const pOwnServer = m_pControlSocket->GetCurrentServer();
	wxASSERT(pOwnServer);

	m_lastListDir = path;

	if (failed)
	{
		CDirectoryListingNotification *pNotification = new CDirectoryListingNotification(path, false, true);
		AddNotification(pNotification);
		m_lastListTime = CTimeEx::Now();

		// On failed messages, we don't notify other engines
		return;
	}

	const CDirectoryCache cache;
	
	CTimeEx changeTime;
	if (!cache.GetChangeTime(changeTime, *pOwnServer, path))
		return;
	
	CDirectoryListingNotification *pNotification = new CDirectoryListingNotification(path, !onList);
	AddNotification(pNotification);
	m_lastListTime = changeTime;

	if (!modified)
		return;

	// Iterate over the other engine, send notification if last listing
	// directory is the same
	for (std::list<CFileZillaEnginePrivate*>::iterator iter = m_engineList.begin(); iter != m_engineList.end(); iter++)
	{
		CFileZillaEnginePrivate* const pEngine = *iter;
		if (!pEngine->m_pControlSocket || pEngine->m_pControlSocket == m_pControlSocket)
			continue;

		const CServer* const pServer = pEngine->m_pControlSocket->GetCurrentServer();
		if (!pServer || *pServer != *pOwnServer)
			continue;

		if (pEngine->m_lastListDir != path)
			continue;

		if (pEngine->m_lastListTime.GetTime().IsValid() && changeTime <= pEngine->m_lastListTime)
			continue;
		
		pEngine->m_lastListTime = changeTime;
		CDirectoryListingNotification *pNotification = new CDirectoryListingNotification(path, true);
		pEngine->AddNotification(pNotification);
	}
}

void CFileZillaEnginePrivate::RegisterFailedLoginAttempt(const CServer& server)
{
	std::list<t_failedLogins>::iterator iter = m_failedLogins.begin();
	while (iter != m_failedLogins.end())
	{
		const wxTimeSpan span = wxDateTime::UNow() - iter->time;
		if (span.GetSeconds() >= m_pOptions->GetOptionVal(OPTION_RECONNECTDELAY) ||
			(iter->host == server.GetHost() && iter->port == server.GetPort()))
		{
			std::list<t_failedLogins>::iterator prev = iter;
			iter++;
			m_failedLogins.erase(prev);
		}
		else
			iter++;
	}

	t_failedLogins failure;
	failure.host = server.GetHost();
	failure.port = server.GetPort();
	failure.time = wxDateTime::UNow();
	m_failedLogins.push_back(failure);
}

unsigned int CFileZillaEnginePrivate::GetRemainingReconnectDelay(const CServer& server)
{
	std::list<t_failedLogins>::iterator iter = m_failedLogins.begin();
	while (iter != m_failedLogins.end())
	{
		const wxTimeSpan span = wxDateTime::UNow() - iter->time;
		const int delay = m_pOptions->GetOptionVal(OPTION_RECONNECTDELAY);
		if (span.GetSeconds() >= delay)
		{
			std::list<t_failedLogins>::iterator prev = iter;
			iter++;
			m_failedLogins.erase(prev);
		}
		else if (iter->host == server.GetHost() && iter->port == server.GetPort())
		{
			return delay * 1000 - span.GetMilliseconds().GetLo();
		}
		else
			iter++;
	}

	return 0;
}

void CFileZillaEnginePrivate::OnTimer(wxTimerEvent& event)
{
	if (!m_pCurrentCommand || m_pCurrentCommand->GetId() != cmd_connect)
	{
		wxFAIL_MSG(_T("CFileZillaEnginePrivate::OnTimer called without pending cmd_connect"));
		return;
	}
	wxASSERT(!IsConnected());

#ifdef __WXDEBUG__
	const CConnectCommand *pConnectCommand = (CConnectCommand *)m_pCurrentCommand;
	wxASSERT(!GetRemainingReconnectDelay(pConnectCommand->GetServer()));
#endif

	ContinueConnect();
}

int CFileZillaEnginePrivate::ContinueConnect()
{
	if (m_pControlSocket)
	{
		delete m_pControlSocket;
		m_pControlSocket = 0;
	}

	const CConnectCommand *pConnectCommand = (CConnectCommand *)m_pCurrentCommand;
	const CServer& server = pConnectCommand->GetServer();
	unsigned int delay = GetRemainingReconnectDelay(server);
	if (delay)
	{
		m_pLogging->LogMessage(Status, _("Delaying connection due to previously failed connection attempt..."));
		m_retryTimer.Start(delay, true);
		return FZ_REPLY_WOULDBLOCK;
	}

	switch (server.GetProtocol())
	{
	case FTP:
	case FTPS:
	case FTPES:
		m_pControlSocket = new CFtpControlSocket(this);
		break;
	case SFTP:
		m_pControlSocket = new CSftpControlSocket(this);
		break;
	case HTTP:
		m_pControlSocket = new CHttpControlSocket(this);
		break;
	default:
		return FZ_REPLY_SYNTAXERROR;
	}

	int res = m_pControlSocket->Connect(server);
	if (m_retryTimer.IsRunning())
		return FZ_REPLY_WOULDBLOCK;

	return res;
}

void CFileZillaEnginePrivate::InvalidateCurrentWorkingDirs(const CServerPath& path)
{
	wxASSERT(m_pControlSocket);
	const CServer* const pOwnServer = m_pControlSocket->GetCurrentServer();
	wxASSERT(pOwnServer);

	for (std::list<CFileZillaEnginePrivate*>::iterator iter = m_engineList.begin(); iter != m_engineList.end(); iter++)
	{
		if (*iter == this)
			continue;

		CFileZillaEnginePrivate* pEngine = *iter;
		if (!pEngine->m_pControlSocket)
			continue;

		const CServer* const pServer = pEngine->m_pControlSocket->GetCurrentServer();
		if (!pServer || *pServer != *pOwnServer)
			continue;

		pEngine->m_pControlSocket->InvalidateCurrentWorkingDir(path);
	}
}


syntax highlighted by Code2HTML, v. 0.9.1