#include "context.h"
#include <cerrno>
#include <functional>
#include <stdexcept>
#include <sstream>

using namespace std::string_literals;

namespace myapp::modbus {

static void checked(std::function<::modbus_t*()> && f)
{
	if (f() == nullptr)
		throw std::runtime_error(::modbus_strerror(errno));
}

static std::string next_token(std::istream& is)
{
	if (is.peek() == '[' && (is.get(), true))
	{
		std::string s;
		if (std::getline(is, s, ']'))
		{
			auto following = is.get();
			if (is.eof() || following == ':')
				return s;
		}

		throw std::runtime_error("Invalid format in connection string");
	}

	std::string s;
	if (std::getline(is, s, ':'))
		return s;

	throw std::runtime_error("Missing parameter(s) in connection string");
}

static std::string substr(std::istringstream& is)
{
	return is.str().substr(is.tellg());
}

static void check_end(std::istream& is)
{
	if (!is.eof())
		throw std::runtime_error("Unexpected parameter follows in connection string");
}

context::context(std::string const& connection_string)
{
	try
	{
		std::istringstream is(connection_string);
		auto comm = next_token(is);
		if (comm == "rtu")
		{
			create_rtu(substr(is));
			return;
		}
		if (comm == "tcp")
		{
			create_tcp_pi(substr(is));
			return;
		}

		throw std::runtime_error("Invalid modbus context");
	}
	catch (std::logic_error& e)
	{
		throw std::runtime_error("Invalid argument given for creating modbus context");
	}
}

context::~context()
{
	::modbus_free(ctx_), ctx_ = nullptr;
}

void context::create_rtu(std::string const& connection_string)
{
	std::istringstream is(connection_string);
	auto device = next_token(is);
	auto baud = std::stoi(next_token(is));
	auto databits = std::stoi(next_token(is));
	auto parity = static_cast<char>(std::toupper(next_token(is).at(0)));
	auto stopbits = std::stoi(next_token(is));
	check_end(is);

	checked([&] { return ctx_ = ::modbus_new_rtu(device.c_str(), baud, parity, databits, stopbits); });
	desc_ = "RTU "s + device + " ("s + std::to_string(baud) + " "s + std::to_string(databits) + parity + std::to_string(stopbits) + ")"s;
}

void context::create_tcp_pi(std::string const& connection_string)
{
	std::istringstream is(connection_string);
	auto node = next_token(is);
	auto service = next_token(is);
	check_end(is);

	checked([&] { return ctx_ = ::modbus_new_tcp_pi(node.c_str(), service.c_str()); });
	desc_ = node.find(':') != std::string::npos
		? "TCP ["s + node + "]:"s + service
		: "TCP "s + node + ":"s + service;
}

void swap(context& lhs, context& rhs) noexcept
{
	using std::swap;
	swap(lhs.ctx_, rhs.ctx_);
	swap(lhs.desc_, rhs.desc_);
}

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

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

} /* namespace myapp::modbus */
