#include "rawtcpserver.h"
#include "utils/stringcompare.h"
#include <cerrno>
#include <cstring>
#include <stdexcept>
#include <poll.h>
#include <unistd.h>
#include <algorithm>
#include <netinet/in.h>
#include <netdb.h>
#include "myapp/modbus/rawdata.h"
#include <cassert>

using namespace std::string_literals;

namespace myapp::modbus {

static void checked(int result, std::string const& prefix)
{
	if (result == -1)
		throw std::runtime_error(prefix + ": " + ::modbus_strerror(errno));
}

raw_tcp_server_impl::socket::~socket()
{
	if (fd_ != -1)
		close(fd_);
}

raw_tcp_server_impl::socket::socket(socket&& o) noexcept
{
	swap(*this, o);
}

raw_tcp_server_impl::socket& raw_tcp_server_impl::socket::operator=(socket&& o) noexcept
{
	swap(*this, o);
	return *this;
}

void swap(raw_tcp_server_impl::socket& lhs, raw_tcp_server_impl::socket& rhs) noexcept
{
	using std::swap;
	swap(lhs.fd_, rhs.fd_);
	swap(lhs.peername_, rhs.peername_);
}

std::string const& raw_tcp_server_impl::socket::peer_name()
{
	if (!peername_.empty())
		return peername_;

	sockaddr_storage sa;
	socklen_t salen = sizeof(sa);
	if (0 != ::getpeername(fd_, (sockaddr*)&sa, &salen))
		throw std::runtime_error("Unable to get peer name");

	char host[NI_MAXHOST];
	char serv[NI_MAXSERV];
	if (0 != ::getnameinfo((sockaddr*)&sa, salen,
		host, NI_MAXHOST, serv, NI_MAXSERV,
		NI_NUMERICHOST | NI_NUMERICSERV))
	{
		throw std::runtime_error("Unable to get name info");
	}

	if (strchr(host, ':'))
		return peername_ = "["s + std::string(host) + "]"s + ":"s + std::string(serv);
	else
		return peername_ = std::string(host) + ":"s + std::string(serv);
}

std::shared_ptr<raw_tcp_server> raw_tcp_server::create(std::shared_ptr<logging::logger> logger, std::string const& connection_string)
{
	try
	{
		if (!utils::starts_with(connection_string, "tcp:"))
			throw std::runtime_error("Not a TCP connection string");

		context ctx(connection_string);
		logger->info("MODBUS TCP", "Created modbus context "s + ctx.description());
		return std::make_shared<raw_tcp_server_impl>(std::move(logger), std::move(ctx));
	}
	catch (std::runtime_error& e)
	{
		logger->error("MODBUS TCP", "Unable to initialize modbus context: "s + e.what());
		throw;
	}
}

raw_tcp_server_impl::raw_tcp_server_impl(std::shared_ptr<logging::logger> logger, context ctx)
  : ctx_(std::move(ctx))
  , logger_(std::move(logger))
{
}

raw_tcp_server_impl::~raw_tcp_server_impl()
{
	modbus_close(ctx_);
}

void raw_tcp_server_impl::listen(int backlog)
{
	if (!listener_.empty())
		throw std::runtime_error("Already listening");

	checked(listener_ = ::modbus_tcp_pi_listen(ctx_, backlog), "tcp listen");
}

void raw_tcp_server_impl::accept()
{
	int sock;

	try 
	{
		checked(sock = modbus_tcp_pi_accept(ctx_, listener_), "accept");
	}
	catch (...)
	{
		// libmodbus 3.0.6 bug - on accept failure closes server socket and sets it to zero (stdout) instead of -1
		// newer versions do not close the socket
		if (listener_ == 0)
			*listener_ = -1;

		throw;
	}

	clients_.emplace_back(sock);
	logger_->info("MODBUS TCP", "Accepted client "s + clients_.back().peer_name());
}

void raw_tcp_server_impl::handle_data(socket& sock)
{
	modbus_set_socket(ctx_, sock);

	int rc;
	auto query = raw_data::tcp();
	checked(rc = modbus_receive(ctx_, &(*query)[0]), "Receive failed on client "s + sock.peer_name());
	if (rc == 0)
		return;

	query->size(rc);
	auto query_pdu = query->data();
	auto slave_id = query_pdu[0];

	auto fun_str = [](int fun) { return "Fun "s + std::to_string(fun & 0x7f) + (fun & 0x80 ? " exception"s : std::string()); };
	auto bytes_str = [](raw_data& adu) { return std::to_string(adu.data_length()) + " bytes"s; };

	logger_->debug("MODBUS TCP", "Client "s + sock.peer_name() +
		" requests " + fun_str(query_pdu[1]) + " ("s + bytes_str(*query) + ") from slave "s + std::to_string(slave_id));

	auto response = data_handler_(*query);
	if (response == nullptr)
		return;

	auto response_pdu = response->data();

    slave_id = response_pdu[0];

	logger_->debug("MODBUS TCP", "Client "s + sock.peer_name() +
		" would receive response " + fun_str(response_pdu[1]) + " ("s + bytes_str(*response) + ") from slave "s + std::to_string(slave_id));

	std::copy_n(&response_pdu[0], response->data_length(), &query_pdu[0]);
	query->data_length(response->data_length());
	query->size(response->data_length() + (&query_pdu[0] - &(*query)[0]));

	checked(send(sock, &(*query)[0], query->size(), MSG_NOSIGNAL),
		"Send failed on client "s + sock.peer_name());
}

void raw_tcp_server_impl::poll()
{
	auto const nfds = 1 + clients_.size();
	struct pollfd fds[nfds];
	auto i_fd = decltype(nfds){0};

	auto set_fd_pollin = [&fds, &i_fd](int sock)
	{
		fds[i_fd].fd = sock;
		fds[i_fd].events = POLLIN;
		++i_fd;
	};

	set_fd_pollin(listener_);
	std::for_each(begin(clients_), end(clients_), set_fd_pollin);

	auto const activefds = ::poll(fds, nfds, -1);
	if (-1 == activefds)
		throw std::runtime_error("poll: "s + strerror(errno));
	if (0 == activefds)
		return;

	i_fd = 0;
	if (fds[i_fd++].revents & POLLIN)
		accept();

	for (; i_fd < nfds; ++i_fd)
	{
		if (fds[i_fd].revents & POLLIN)
		{
			auto iter = std::find_if(begin(clients_), end(clients_),
				[&](socket const& s) { return s == fds[i_fd].fd; });
			assert(iter != end(clients_));

			try 
			{
				handle_data(*iter);
			}
			catch (std::runtime_error& e)
			{
				logger_->info("MODBUS TCP", "Closing connection: "s + e.what());
				clients_.erase(iter);
				fds[i_fd].fd = -fds[i_fd].fd;
			}
		}
	}
}

void raw_tcp_server_impl::set_data_handler(data_handler_t value) noexcept
{
	data_handler_ = std::move(value);
}


}
