#include "connection.h"
#include "context.h"
#include <algorithm>
#include <cerrno>
#include <stdexcept>

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));
}

static void maybe_exception_response(int result)
{
    if (result != -1)
        return;

    switch (errno)
    {
    default: throw std::runtime_error(::modbus_strerror(errno));

    case EMBXILFUN: throw std::runtime_error("EMBXILFUN");
    case EMBXILADD: throw std::runtime_error("EMBXILADD");
    case EMBXILVAL: throw std::runtime_error("EMBXILVAL");
    case EMBXSFAIL: throw std::runtime_error("EMBXSFAIL");
    case EMBXACK: throw std::runtime_error("EMBXACK");
    case EMBXSBUSY: throw std::runtime_error("EMBXSBUSY");
    case EMBXNACK: throw std::runtime_error("EMBXNACK");
    case EMBXMEMPAR: throw std::runtime_error("EMBXMEMPAR");
    case EMBXGPATH: throw std::runtime_error("EMBXGPATH");
    case EMBXGTAR: throw std::runtime_error("EMBXGTAR");

    case EMBBADCRC: throw std::runtime_error("EMBBADCRC");
    case EMBBADDATA: throw std::runtime_error("EMBBADDATA");
    case EMBBADEXC: throw std::runtime_error("EMBBADEXC");
    case EMBUNKEXC: throw std::runtime_error("EMBUNKEXC");
    case EMBMDATA: throw std::runtime_error("EMBMDATA");
    }
}

std::shared_ptr<connection> connection::create(std::shared_ptr<logging::logger> logger, std::string const& connection_string)
{
	try
	{
		context ctx(connection_string);
		logger->info("MODBUS", "Created modbus context "s + ctx.description());
		return std::make_shared<connection_impl>(std::move(logger), std::move(ctx));
	}
	catch (std::runtime_error& e)
	{
		logger->error("MODBUS", "Unable to initialize modbus context: "s + e.what());
		throw;
	}
}

connection_impl::connection_impl(std::shared_ptr<logging::logger> logger, context ctx)
  : context_(std::move(ctx))
  , logger_(std::move(logger))
  , poll_(poll::create())
{
    // modbus_set_debug(context_, TRUE);

    constexpr auto recovery_mode = static_cast<modbus_error_recovery_mode>(MODBUS_ERROR_RECOVERY_LINK | MODBUS_ERROR_RECOVERY_PROTOCOL);
    checked(::modbus_set_error_recovery(context_, recovery_mode), "Error recovery setting failed");
}

void connection_impl::flush()
{
	checked(::modbus_flush(context_), "Flush failed on "s  + context_.description());
}

void connection_impl::connect()
{
	try 
	{
		checked(::modbus_connect(context_), "Connection failed on "s + context_.description());
	}
	catch (std::runtime_error& e)
	{
		logger_->error("MODBUS", e.what());
		throw;
	}

	logger_->info("MODBUS", "Connected "s + context_.description());
	
	try
	{
		flush();
	}
	catch (std::runtime_error& e)
	{
		logger_->warning("MODBUS", e.what());
	}
}

void connection_impl::close()
{
    logger_->info("MODBUS", "Closing connection "s + context_.description());
    ::modbus_close(context_);
}

std::shared_ptr<poll> connection_impl::get_poller() noexcept
{
    return poll_;
}

std::shared_ptr<data_access> connection_impl::get_data_access() noexcept
{
    return shared_from_this();
}

void connection_impl::set_slave(int addr)
{
    checked(::modbus_set_slave(context_, addr), "Set slave id failed");
}

std::vector<uint8_t> connection_impl::read_coils(int slave_id, int starting_address, int quantity)
{
    std::vector<uint8_t> status(quantity);
    set_slave(slave_id);
    maybe_exception_response(::modbus_read_bits(context_, starting_address, quantity, &status[0]));
    return status;
}

std::vector<uint8_t> connection_impl::read_discrete_inputs(int slave_id, int starting_address, int quantity)
{
    std::vector<uint8_t> status(quantity);
    set_slave(slave_id);
    maybe_exception_response(::modbus_read_input_bits(context_, starting_address, quantity, &status[0]));
    return status;
}

std::vector<uint16_t> connection_impl::read_holding_registers(int slave_id, int starting_address, int quantity)
{
    std::vector<uint16_t> values(quantity);
    set_slave(slave_id);
    maybe_exception_response(::modbus_read_registers(context_, starting_address, quantity, &values[0]));
    return values;
}

std::vector<uint16_t> connection_impl::read_input_registers(int slave_id, int starting_address, int quantity)
{
    std::vector<uint16_t> values(quantity);
    set_slave(slave_id);
    maybe_exception_response(::modbus_read_input_registers(context_, starting_address, quantity, &values[0]));
    return values;
}

void connection_impl::write_single_coil(int slave_id, int address, uint8_t value)
{
    set_slave(slave_id);
    maybe_exception_response(::modbus_write_bit(context_, address, value));
}

void connection_impl::write_single_register(int slave_id, int address, uint16_t value)
{
    set_slave(slave_id);
    maybe_exception_response(::modbus_write_register(context_, address, value));
}

void connection_impl::write_multiple_coils(int slave_id, int starting_address, std::vector<uint8_t> values)
{
    set_slave(slave_id);
    maybe_exception_response(::modbus_write_bits(context_, starting_address, values.size(), &values[0]));
}

void connection_impl::write_multiple_registers(int slave_id, int starting_address, std::vector<uint16_t> values)
{
    set_slave(slave_id);
    maybe_exception_response(::modbus_write_registers(context_, starting_address, values.size(), &values[0]));
}

std::vector<uint16_t> connection_impl::read_write_multiple_registers(int slave_id,
    int read_starting_address, int quantity_to_read,
    int write_starting_address, std::vector<uint16_t> values_to_write)
{
    std::vector<uint16_t> values_to_read(quantity_to_read);
    set_slave(slave_id);
    maybe_exception_response(::modbus_write_and_read_registers(context_,
        write_starting_address, values_to_write.size(), &values_to_write[0],
        read_starting_address, quantity_to_read, &values_to_read[0]));
    return values_to_read;
}


} /* namespace myapp::modbus */
