#include "config.h"

// Implementation
#include "Socket.hpp"

// STDC++
#include <cassert>
#include <fstream>

// Local
#include "Text.hpp"

namespace UI {
namespace Util {

Socket::Socket(int fd, bool closeFd)
#ifndef WIN32
	:FileDescriptor(fd, closeFd)
#else
	:fd_(fd), closeFd_(closeFd)
#endif
{
#ifdef WIN32
	// clear inner buffer
	sendBuffer.clear();
#endif
}

#ifdef WIN32
void Socket::sendStoredBuffer()
{
	// all stored data must be sent now
	if(sendBuffer.size() > 0)
	{
		void *pBuffer	= (void*)new char[sendBuffer.size()];
		void *tmp = pBuffer;
		for(size_t i = 0; i < sendBuffer.size(); i++)
			((char*)pBuffer)[i] = sendBuffer[i];

		int	nSent		= 0;
		int nRemaining	= sendBuffer.size();
		while( nRemaining > 0 )
		{
			nSent			= ::send( fd_, (const char*)pBuffer, nRemaining, 0 );
			if(nSent > 0)
			{
				pBuffer			= (char*)pBuffer + nSent;
				nRemaining		-= nSent;
			}
			else
			{
				delete [] tmp;
				UI_THROW_CODE(FileDescriptor::WriteErr_, "Error sending response, sent " + tos(sendBuffer.size() - nRemaining) + " out of " + tos(sendBuffer.size()));
			}
		}
		delete [] tmp;

		// clear buffer
		sendBuffer.clear();
	}
}

Socket::~Socket()
{
	// send buffer if any
	sendStoredBuffer();

	if (closeFd_)
	{
		if (::shutdown(fd_, SD_SEND) != 0)
		{
			::closesocket(fd_);
			// No exception possible -- destructor.
		}
	}
}
#endif

std::string Socket::getId(bool const & peer) const
{
	try
	{
		// Get addr struct from file descriptor
		struct sockaddr_storage addr;
		memset(&addr, 0, sizeof(addr));
		socklen_t addrLen(sizeof(addr));;
		peer ? Sys::getpeername(getFd(), (sockaddr *) &addr, &addrLen) : Sys::getsockname(getFd(), (sockaddr *) &addr, &addrLen);

		// Get human-readable string
		return Sys::getnameinfo((sockaddr *) &addr, addrLen);
	}
	catch (std::exception const & e)
	{
		return e.what();
	}
}

std::string Socket::getPeerId() const
{
	return getId(true);
}

Socket & Socket::setRcvTimeout(long int seconds, long int microseconds)
{
	struct timeval const to = {seconds, microseconds}; // 1/1 seconds, 1/1000000 seconds
	Sys::setsockopt_to(fd_, SOL_SOCKET, SO_RCVTIMEO, to);
	return *this;
}

Socket & Socket::setSndTimeout(long int seconds, long int microseconds)
{
	struct timeval const to = {seconds, microseconds}; // 1/1 seconds, 1/1000000 seconds
	Sys::setsockopt_to(fd_, SOL_SOCKET, SO_SNDTIMEO, to);
	return *this;
}


Socket & Socket::setUnblock(bool unblock)
{
	try
	{
		if (unblock)
		{
#ifndef WIN32
			Sys::fcntl(fd_, F_SETFL, O_NONBLOCK);
#endif
		}
		return *this;
	}
	catch(std::exception const & e)
	{
		UI_THROW_CODE(FileDescriptor::UnblockErr_, "Can't unblock " + getId() + ": " + e.what());
	}
}


Socket & Socket::bind()
{
	UI_THROW_CODE(FileDescriptor::ConnectErr_, "Don't know how to bind socket " + getId());
}

Socket & Socket::connect()
{
	UI_THROW_CODE(FileDescriptor::ConnectErr_, "Don't know how to connect socket " + getId());
}

Socket & Socket::listen(int backlog)
{
	try
	{
		Sys::listen(fd_, backlog);
		return *this;
	}
	catch(std::exception const & e)
	{
		UI_THROW_CODE(FileDescriptor::ListenErr_, "Can't set " + getId() + " to listen: " + e.what());
	}
}

int Socket::accept(long int toSeconds, long int toMicroSeconds)
{
	if (toSeconds == 0 && toMicroSeconds == 0)
	{
		return ::accept(fd_, 0, 0);
	}
	else
	{
		// We need to use select to set a timeout
		fd_set fdSet;
		FD_ZERO(&fdSet);
		FD_SET(fd_, &fdSet);
		struct timeval timeout = {toSeconds, toMicroSeconds}; // seconds, microseconds
		int selectResult(::select(fd_+1, &fdSet, 0, 0, &timeout));
		if (selectResult > 0)
		{
			assert(selectResult == 1);
			return ::accept(fd_, 0, 0);
		}
		else
		{
			return selectResult;
		}
	}
}

int Socket::shutdown(int how, bool doThrow)
{
	int const res(::shutdown(fd_, how));
	if (res != 0 && doThrow)
	{
		UI_THROW_CODE(FileDescriptor::ShutdownErr_, "Error on shutdown(2) of fd " + tos(fd_));
	}
	return res;
}

ssize_t Socket::recv(void * const buf, size_t len, int flags)
{
#ifdef WIN32
	// send stored buffer if any
	sendStoredBuffer();
	// receive something
	size_t result(::recv(fd_, (char*)buf, len, flags));
	if (result < 0) { throw UI::Exception("Error recv(2)ing from socket fd " + tos(fd_)); }
	return result;
#else
	return Sys::recv(fd_, buf, len, flags);
#endif
}

ssize_t Socket::send(void const * const msg, size_t len, int flags)
{
#ifdef WIN32
	// put all data in the inner buffer and send nothing
	for(size_t i = 0; i < len; i++)
		sendBuffer.push_back(((char*)msg)[i]);
	// send the data if around 64k
	if(sendBuffer.size() > 60000)
		sendStoredBuffer();
	// return send OK
	if (len < 0) { throw UI::Exception("Error send(2)ing to socket fd " + tos(fd_)); }
	return len;
#else
	return Sys::send(fd_, msg, len, flags);
#endif
}

std::streamsize Socket::read(void * const buf, std::streamsize count)
{
	try { return static_cast<std::streamsize>(recv(buf, count)); }
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::ReadErr_, "Error reading from socket " + getId() + ": " + e.what()); }
}

std::streamsize Socket::write(void const * const buf, std::streamsize count)
{
	try { return static_cast<std::streamsize>(send(buf, count)); }
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::WriteErr_, "Error writing to socket " + getId() + ": " + e.what()); }
}


INetSocket::INetSocket(std::string const & host, unsigned int port, bool closeFd, bool reuseAddr)
	:Socket(-1, closeFd)
	,addrInfo_(0)
	,addr_(0)
{
	try
	{
		// Generate hints struct (for getaddrinfolater).
		struct addrinfo hints;
		memset(&hints, 0, sizeof(struct addrinfo));
		hints.ai_family = AF_UNSPEC;     // IPv4 or IPv6
		hints.ai_socktype = SOCK_STREAM; // TCP stream
		hints.ai_flags = AI_PASSIVE;
		hints.ai_protocol = 0;
		hints.ai_canonname = 0;
		hints.ai_addr = 0;
		hints.ai_next = 0;

		// Get addr info from host & port
		std::string const service(tos(port));
		Sys::getaddrinfo(host.c_str(), service.c_str(), &hints, &addrInfo_);

		// Try open socket in the given order
		addr_ = addrInfo_;
		while (addr_ != 0 && (fd_ = ::socket(addr_->ai_family, addr_->ai_socktype, addr_->ai_protocol)) < 0)
		{
			addr_ = addr_->ai_next;
		}
		if (fd_ < 0) { UI_THROW_CODE(FileDescriptor::OpenErr_, "Can't open network address: " + host + ":" + service); }

		// Optionally set SO_REUSEADDR flag
		if (reuseAddr)
		{
			int const one(1);
			Sys::setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
		}
	}
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::OpenErr_, e.what()); }
}

INetSocket::INetSocket(int fd, bool closeFd)
	:Socket(fd, closeFd)
	,addrInfo_(0)
	,addr_(0)
{}

INetSocket::~INetSocket()
{
	::freeaddrinfo(addrInfo_);
}

INetSocket & INetSocket::bind()
{
	try
	{
		Sys::bind(fd_, addr_->ai_addr, addr_->ai_addrlen);
		return *this;
	}
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::BindErr_, e.what()); }
}

INetSocket & INetSocket::connect()
{
	try
	{
		Sys::connect(fd_, addr_->ai_addr, addr_->ai_addrlen);
		return *this;
	}
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::ConnectErr_, e.what()); }
}

#ifndef WIN32
UnixSocket::UnixSocket(std::string const & path, bool closeFd)
	:Socket(-1, closeFd)
	,silentUnlink_(false)
{
	try
	{
		// Create Socket
		fd_ = Sys::socket(PF_UNIX, SOCK_STREAM, 0);

		// Initialize Unix Address
		if (strlen(path.c_str()) >= sizeof(unSa_.sun_path))
		{
			UI_THROW_CODE(OpenErr_, "Path name for unix socket too long");
		}
		unSa_.sun_family = AF_LOCAL;
		std::strncpy(unSa_.sun_path, path.c_str(), sizeof(unSa_.sun_path)-1);
	}
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::OpenErr_, "Can't open unix socket " + path + ": " + e.what()); }
}

UnixSocket::UnixSocket(int fd, bool closeFd)
	:Socket(fd, closeFd)
	,silentUnlink_(false)
{
	// We need to protect this from being undefined for getId().
	unSa_.sun_path[0] = '\0';
}

UnixSocket::~UnixSocket()
{
	if (silentUnlink_) { try { Sys::unlink(unSa_.sun_path); } catch(...) {}; }
}

UnixSocket & UnixSocket::bind()
{
	return unixBind();
}

UnixSocket & UnixSocket::unixBind(uid_t uid, gid_t gid, mode_t mode, bool silentUnlink)
{
	try
	{
		silentUnlink_ = silentUnlink;
		if (silentUnlink_) { try { Sys::unlink(unSa_.sun_path); } catch(...) {}; }

		// Bind Socket to Unix Address; this will, too, create the socket file in the filesystem
		Sys::bind(fd_, (struct sockaddr *) &unSa_, sizeof(unSa_.sun_family) + (size_t) strlen(unSa_.sun_path));

		// Set both uid and user's default group id
		Sys::chown(unSa_.sun_path, uid, gid);

		// Set permissions
		Sys::chmod(unSa_.sun_path, mode);

		return *this;
	}
	catch(std::exception const & e)
	{
		UI_THROW_CODE(FileDescriptor::BindErr_, "Can't bind " + getId() + ": " + e.what());
	}
}

UnixSocket & UnixSocket::connect()
{
	try
	{
		Sys::connect(fd_, (struct sockaddr *) &unSa_, sizeof(unSa_.sun_family) + (size_t) strlen(unSa_.sun_path));
		return *this;
	}
	catch(std::exception const & e) { UI_THROW_CODE(FileDescriptor::ConnectErr_, "Can't connect " + getId() + ": " + e.what()); }
}
#endif

/** @brief socketpair(2) abstraction. */
SocketPair::SocketPair(bool const & closeFd)
	:closeFd_(closeFd)
{
	Sys::socketpair(AF_UNIX, SOCK_STREAM, 0, sv_);
}

SocketPair::~SocketPair()
{
	FileDescriptor::fdClose(sv_[0], "SocketPair", closeFd_);
	FileDescriptor::fdClose(sv_[1], "SocketPair", closeFd_);
}
int const & SocketPair::first()  const { return sv_[0]; }
int const & SocketPair::second() const { return sv_[1]; }

}}
