#include <unistd.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netdb.h>
#include <fcntl.h>

#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#include <loguru.hpp>
#include <ftl/uri.hpp>
#include <ftl/exception.hpp>

using ftl::net::internal::Socket;
using ftl::net::internal::SocketAddress;

/// resolve address for OS socket calls, return true on success
bool ftl::net::internal::resolve_inet_address(const std::string &hostname, int port, SocketAddress &address) {
	addrinfo hints = {}, *addrs;

	// TODO: use uri for hints. Fixed values used here
	// should work as long only TCP is used.
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	
	auto rc = getaddrinfo(hostname.c_str(), std::to_string(port).c_str(), &hints, &addrs);
	if (rc != 0 || addrs == nullptr) return false;
	
	address.len = (socklen_t) addrs->ai_addrlen;
	memcpy(&address.addr, addrs->ai_addr, address.len);
	freeaddrinfo(addrs);
	return true;
}

// Socket

Socket::Socket(int domain, int type, int protocol) :
		status_(STATUS::UNCONNECTED), fd_(-1), family_(domain), err_(0) {

	int retval = socket(domain, type, protocol);

	if (retval > 0) {
		fd_ = retval;
	} else {
		LOG(ERROR) << ("socket() failed");
		throw FTL_Error("socket: " + get_error_string());
	}
}

bool Socket::is_valid() { return status_ != STATUS::INVALID; }

ssize_t Socket::recv(char *buffer, size_t len, int flags) {
	return ::recv(fd_, buffer, len, flags);
}

ssize_t Socket::send(const char* buffer, size_t len, int flags) {
	return ::send(fd_, buffer, len, flags);
}

ssize_t Socket::writev(const struct iovec *iov, int iovcnt) {
	return ::writev(fd_, iov, iovcnt);
}

int Socket::bind(const SocketAddress &addr) {
	auto retval = ::bind(fd_, &addr.addr, addr.len);
	if (retval) {
		status_ = Socket::OPEN;
	}
	return retval;
}

int Socket::listen(int backlog) {
	return ::listen(fd_, backlog);
}

Socket Socket::accept(SocketAddress &addr) {
	addr.len = sizeof(SocketAddress);
	Socket socket;
	int retval = ::accept(fd_, &(addr.addr), &(addr.len));
	if (retval > 0) {
		socket.status_ = STATUS::OPEN;
		socket.fd_ = retval;
		socket.family_ = family_;
	} else {
		LOG(ERROR) << "accept returned error: " << strerror(errno); 
		socket.status_ = STATUS::INVALID;
	}
	return socket;
}

int Socket::connect(const SocketAddress& address) {

	int err = 0;
	if (status_ == STATUS::UNCONNECTED) {
		err = ::connect(fd_, &address.addr, address.len);
		if (err == 0) {
			status_ = STATUS::OPEN;
			return 0;
		}
		else 
		{
			if (errno == EINPROGRESS) {
				status_ = STATUS::OPEN;	// close() will be called by destructor
										// add better status code?
				return -1;
			} else {
				status_ = STATUS::CLOSED;
				::close(fd_);
			}
		}
	}
	return err;
}

int Socket::connect(const SocketAddress &address, int timeout) {
	if (timeout <= 0) {
		return connect(address);
	}

	bool blocking = is_blocking();
	if (blocking) set_blocking(false);
	
	auto rc = connect(address);
	if (rc < 0) {
		if (errno == EINPROGRESS) {
			fd_set myset;
			fd_set errset; 
			FD_ZERO(&myset); 
			FD_SET(fd_, &myset);
			FD_ZERO(&errset); 
			FD_SET(fd_, &errset); 

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

			rc = select(fd_+1u, NULL, &myset, &errset, &tv); 
			if (FD_ISSET(fd_, &errset)) rc = -1;
		}
	}

	if (blocking)
		set_blocking(true);

	if (rc < 0) {
		::close(fd_);
		status_ = STATUS::CLOSED;
		LOG(ERROR) << "socket error: " << strerror(errno);
		return rc;
	}

	return 0;
}

/// Close socket (if open). Multiple calls are safe.
bool Socket::close() {
	if (is_valid() && status_ != STATUS::CLOSED) {
		status_ = STATUS::CLOSED;
		return ::close(fd_) == 0;
	} else if (status_ != STATUS::CLOSED) {
		LOG(INFO) << "close() on non-valid socket";
	}
	return false;
}

int Socket::setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
	return ::setsockopt(fd_, level, optname, optval, optlen);
}

int Socket::getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
	return ::getsockopt(fd_, level, optname, optval, optlen);
}

void Socket::set_blocking(bool val) {
	auto arg = fcntl(fd_, F_GETFL, NULL);
	arg = val ? (arg | O_NONBLOCK) : (arg & ~O_NONBLOCK);
	fcntl(fd_, F_SETFL, arg);
}

bool Socket::is_blocking() {
	return fcntl(fd_, F_GETFL, NULL) & O_NONBLOCK;
}

bool Socket::is_fatal(int code) {
	int e = (code != 0) ? code : errno;
	return !(e == EINTR || e == EWOULDBLOCK || e == EINPROGRESS);
}

std::string Socket::get_error_string(int code) {
	return strerror((code != 0) ? code : errno);
}

// TCP socket

Socket ftl::net::internal::create_tcp_socket() {
	return Socket(AF_INET, SOCK_STREAM, 0);
}

std::string ftl::net::internal::get_host(SocketAddress& addr) {
	char hbuf[1024];
	int err = getnameinfo(&(addr.addr), addr.len, hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD);
	if (err == 0) { return std::string(hbuf); }
	else if (err == EAI_NONAME) { return ftl::net::internal::get_ip(addr); }
	else { LOG(WARNING) << "getnameinfo(): " << gai_strerror(err) << " (" << err << ")"; }
	return "unknown";
}

SocketAddress Socket::getsockname() {
	SocketAddress addr;
	auto* a = reinterpret_cast<struct sockaddr*>(&(addr.addr));
	::getsockname(fd_, a, &(addr.len));
	return addr;
}

std::string ftl::net::internal::get_ip(SocketAddress& addr) {
	auto* addr_in = reinterpret_cast<struct sockaddr_in*>(&(addr.addr));
	std::string address(inet_ntoa(addr_in->sin_addr));
	return address;
}

int ftl::net::internal::get_port(SocketAddress& addr) {
	auto* addr_in = reinterpret_cast<struct sockaddr_in*>(&(addr.addr));
	return htons(addr_in->sin_port);
}