/*
 * tcpconnect.cpp
 *
 *  Created on: 27.02.2010
 *      Author: ed
 */

#define LOCAL_DEBUG
#include "debug.h"

#include "meta.h"
#include "tcpconnect.h"

#include "acfg.h"
#include "caddrinfo.h"
#include <signal.h>
#include "fileio.h"
#include "fileitem.h"
#include "cleaner.h"

using namespace MYSTD;

//#warning FIXME, hack
//#define NOCONCACHE

tcpconnect::tcpconnect() :	m_conFd(-1), m_pConnStateObserver(NULL)
{
	m_proxy = acfg::proxy_info.sHost.empty() ? NULL : &acfg::proxy_info;
}

tcpconnect::~tcpconnect()
{
	LOGSTART("tcpconnect::~tcpconnect, terminating outgoing connection class");
	Disconnect();
}



static int connect_timeout(int sockfd, const struct sockaddr *addr, socklen_t addrlen, time_t timeout)
{
	long stflags;
	struct timeval tv;
	fd_set wfds;
	int res;

	tv.tv_sec = timeout;
	tv.tv_usec = 0;

	if ((stflags = fcntl(sockfd, F_GETFL, NULL)) < 0)
		return -1;

	// Set to non-blocking mode.
	if (fcntl(sockfd, F_SETFL, stflags|O_NONBLOCK) < 0)
		return -1;

	res = connect(sockfd, addr, addrlen);
	if (res < 0) {
		if (EINPROGRESS == errno) {
			for (;;) {
				// Wait for connection.
				FD_ZERO(&wfds);
				FD_SET(sockfd, &wfds);
				res = select(sockfd+1, NULL, &wfds, NULL, &tv);
				if (res < 0) {
					if (EINTR != errno)
						return -1;
				} else if (res > 0) {
					// Socket selected for writing.
					int err;
					socklen_t optlen = sizeof(err);

					if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (void *)&err, &optlen) < 0)
						return -1;

					if (err) {
						errno = err;
						return -1;
					}

					break;
				} else {
					// Timeout.
					errno = ETIMEDOUT;
					return -1;
				}
			}
		} else {
			return -1;
		}
	}

	// Set back to original mode, which may or may not have been blocking.
	if (fcntl(sockfd, F_SETFL, stflags) < 0)
		return -1;

	return 0;
}

bool tcpconnect::Connect(const string & sHostname, const string & sPort, string & sErrorMsg)
{
	LOGSTART2("tcpconnect::_Connect", "hostname: " << sHostname);
	CAddrInfo::SPtr dns = m_proxy ?
			CAddrInfo::CachedResolve(m_proxy->sHost, m_proxy->sPort, sErrorMsg)
			: CAddrInfo::CachedResolve(sHostname, sPort, sErrorMsg);
	if(!dns)
	{
		USRDBG(4, sErrorMsg);
		return false; // sErrorMsg got the info already, no other chance to fix it
	}

	::signal(SIGPIPE, SIG_IGN);

	// always consider first family, afterwards stop when no more specified
	for (UINT i=0; i< _countof(acfg::conprotos) && (0==i || acfg::conprotos[i]!=PF_UNSPEC); ++i)
	{
		for (struct addrinfo *pInfo = dns->m_addrInfo; pInfo; pInfo = pInfo->ai_next)
		{
			if (acfg::conprotos[i] != PF_UNSPEC && acfg::conprotos[i] != pInfo->ai_family)
				continue;

			ldbg("Creating socket for " << sHostname);

			if (pInfo->ai_socktype != SOCK_STREAM || pInfo->ai_protocol != IPPROTO_TCP)
				continue;

			Disconnect();

			m_conFd = ::socket(pInfo->ai_family, pInfo->ai_socktype, pInfo->ai_protocol);
			if (m_conFd < 0)
				continue;

#ifndef NO_TCP_TUNNING
			{
				int yes(1);
				::setsockopt(m_conFd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
				::setsockopt(m_conFd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));
			}
#endif

			if (::connect_timeout(m_conFd, pInfo->ai_addr, pInfo->ai_addrlen, acfg::nettimeout) < 0)
				continue;

			ldbg("connect() ok");
			set_nb(m_conFd);
			m_sHostName = sHostname;
			m_sPort = sPort;
			return true;
		}
	}

#ifdef MINIBUILD
	sErrorMsg = "500 Connection failure";
#else
	// format the last available error message for the user
	sErrorMsg=aclog::errnoFmter("500 Connection failure: ");
#endif
	ldbg("Force reconnect, con. failure");
	Disconnect();
	return false;
}

void tcpconnect::Disconnect()
{
	LOGSTART("tcpconnect::_Disconnect");

	if(m_conFd>=0 && m_pConnStateObserver)
		m_pConnStateObserver->JobRelease();

	m_pConnStateObserver=NULL;
	m_lastFileKeyPath.clear();

	forceShutdownClose(m_conFd);
}

void termsocket(int fd)
{
	LOGSTART2s("::termsocket", fd);
	if (fd < 0)
		return;

	fcntl(fd, F_SETFL, ~O_NONBLOCK & fcntl(fd, F_GETFL));
	::shutdown(fd, SHUT_WR);
	char buf[40];
	LOG("waiting for peer to react");
	while(true)
	{
		int r=recv(fd, buf, 40, MSG_WAITALL);
		if(0 == r)
			break;
		if(r < 0)
		{
			if(errno == EINTR)
				continue;
			break; // XXX error case, actually
		}
	}

	while (0 != ::close(fd))
	{
		if (errno != EINTR)
			break;
	};
}

lockable conPoolMx;
using namespace MYSTD;
struct tHostHint // could derive from pair but prefer to save some bytes with references
{
	cmstring &pHost, &pPort;
	inline tHostHint(cmstring &h, cmstring &p, time_t d=0) : pHost(h), pPort(p) {};
	bool operator<(const tHostHint &b) const
	{
		int rel=pHost.compare(b.pHost);
		return rel ? rel < 0 : pPort < b.pPort;
	}
};
typedef multimap<tHostHint, MYSTD::pair<tTcpHandlePtr, time_t> > tConPool;
tConPool spareConPool;

tTcpHandlePtr tcpconnect::CreateConnected(cmstring &sHostname, cmstring &sPort,
		mstring &sErrOut, bool *pbSecondHand, acfg::tRepoData::IHookHandler *pStateTracker)
{
	tTcpHandlePtr p;
	bool bReused=false;
	tHostHint key(sHostname, sPort);

#ifdef NOCONCACHE
	p.reset(new tcpconnect);
	if(!p || !p->Connect(sHostname, sPort, sErrOut) || p->GetFD()<0) // failed or worthless
		p.reset();
#else
	{
		lockguard __g(conPoolMx);
		tConPool::iterator it=spareConPool.find(key);
		if(spareConPool.end() == it)
		{
			p.reset(new tcpconnect);
			if(!p || !p->Connect(sHostname, sPort, sErrOut) || p->GetFD()<0) // failed or worthless
				p.reset();
		}
		else
		{
			p=it->second.first;
			spareConPool.erase(it);
			bReused = true;
		}
	}
#endif

	if(p && pStateTracker)
	{
		p->m_pConnStateObserver = pStateTracker;
		pStateTracker->JobConnect();
	}

	if(pbSecondHand)
		*pbSecondHand = bReused;

	return p;
}

void tcpconnect::RecycleIdleConnection(tTcpHandlePtr & handle)
{
	if(!handle)
		return;

	if(handle->m_pConnStateObserver)
	{
		handle->m_pConnStateObserver->JobRelease();
		handle->m_pConnStateObserver = NULL;
	}

#ifndef NOCONCACHE
	time_t now=GetTime();
	lockguard __g(conPoolMx);

	// a DOS?
	if(spareConPool.size()<50)
	{
		spareConPool.insert(make_pair(tHostHint(handle->GetHostname(), handle->GetPort()),
				make_pair(handle, now)));
	}
#endif

	handle.reset();
}




time_t tcpconnect::ExpireCache()
{
	lockguard __g(conPoolMx);
	time_t now=GetTime();

	fd_set rfds;
	FD_ZERO(&rfds);
	int nMaxFd=0;

	// either drop the old ones, or stuff them into a quick select call to find the good sockets
	for (tConPool::iterator it = spareConPool.begin(); it != spareConPool.end();)
	{
		if (now >= (it->second.second + TIME_SOCKET_EXPIRE_CLOSE))
			spareConPool.erase(it++);
		else
		{
			int fd = it->second.first->GetFD();
			FD_SET(fd, &rfds);
			nMaxFd = max(nMaxFd, fd);
			++it;
		}
	}
	// if they have to send something, that must the be the CLOSE signal
	struct timeval tv;
	tv.tv_sec = 0;
	tv.tv_usec = 1;
	int r=select(nMaxFd + 1, &rfds, NULL, NULL, &tv);
	// on error, also do nothing, or stop when r fds are processed
	for (tConPool::iterator it = spareConPool.begin(); r>0 && it != spareConPool.end(); r--)
	{
		if(FD_ISSET(it->second.first->GetFD(), &rfds))
			spareConPool.erase(it++);
		else
			++it;
	}

	return spareConPool.empty() ? cleaner::never : GetTime()+TIME_SOCKET_EXPIRE_CLOSE/4+1;
}

void tcpconnect::KillLastFile()
{
#ifndef MINIBUILD
	if (m_lastFileKeyPath.empty())
		return;

	tFileItemPtr p(fileitem::GetRegisteredFileItem(m_lastFileKeyPath));
	if (!p)
		return;

	p->SetupClean(true);
	p->Unreg();
#endif
}

