/* GPC (C) 2017-2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: BinaryProtocolConnection.cpp 2490 2018-03-19 07:07:56Z stephane $
 */

#include "GPC.hpp"


BinaryProtocolConnection::BinaryProtocolConnection(const std::string ip, const int tcp) :
	port(tcp),
	ip_address(ip),
	name(ip_address + ":" + std::to_string(port))
{
	return;
}


BinaryProtocolConnection::~BinaryProtocolConnection(void)
{
	return;
}


BinaryProtocolConnection &BinaryProtocolConnection::connect(void)
{
	LOG_MSG("establishing a TCP connection to IncJet device at " + name);

	const bool result = sock.connect(ip_address, port);
	if (result == false)
	{
		/// @throw Lox::Exception if the TCP connection fails
		throw Lox::Exception(LOX_WHERE, "Failed to establish a TCP connection to IncJet device at " + name + ".");
	}

	return *this;
}


BinaryProtocolConnection &BinaryProtocolConnection::initiate_communications(void)
{
	// very first thing we should see from the device is ...?
	int rc = sock.waitUntilReady(true, 1000);
	if (rc == -1)
	{
		// error with socket?
		/// @throw Lox::Exception if there is an error while waiting to read from the socket
		throw Lox::Exception(LOX_WHERE, "Error while waiting to read from the connection to the IncJet device at " + name + ".");
	}
	if (rc == 0)
	{
		// timeout?
		/// @throw Lox::Exception if there was a timeout while waiting to read from the socket
		throw Lox::Exception(LOX_WHERE, "Timed out while waiting to read from the connection to the IncJet device at " + name + ".");
	}
	if (rc != 1)
	{
		// unknown (undocumented) problem?
		/// @throw Lox::Exception if there was an unknown/undocumented problem while waiting to read from the socket
		throw Lox::Exception(LOX_WHERE, "Unknown error waiting to read from the connection to the IncJet device at " + name + ".");
	}

	// otherwise, if we get here, then everything seems to be working correctly

	// we expect the very first message to be "power up"
	VBytes v(1000, '\0');
	const int bytes_read = sock.read(v.data(), v.size(), false);
	BinaryProtocol bp(v, bytes_read);
	if (bp.code != BinaryProtocol::ECode::kPowerUpMsg)
	{
		/// @throw Lox::Exception if the first thing read from the device is unexpected
		throw Lox::Exception(LOX_WHERE, "Unexpected command read from the IncJet device at " + name + ".");
	}

	// we need to send back an empty PowerUpMsg
	// (I don't know why, but that is what the original software does!)
	bp.create(BinaryProtocol::ECode::kPowerUpMsg);
	const size_t bytes_written = sock.write(bp.content.data(), bp.content.size());
	LOG_MSG("bytes written to " << name << ": " << bytes_written);
	if (bytes_written != bp.content.size())
	{
		/// @throw Lox::Exception if we fail to write to the socket
		throw Lox::Exception(LOX_WHERE, "Failed to write the required number of bytes to the IncJet device at " + name + ".");
	}

	// ...otherwise, consider ourselves fully connected!

	return *this;
}


BinaryProtocolConnection &BinaryProtocolConnection::send_file(const bool is_ijs, const VBytes &file_contents)
{
	LOG_MSG("sending " << (is_ijs ? ".ijs" : ".ijb") << " file");

	/* Sending .ijb and .ijs files looks like this:
	 *
	 * - 1 byte:	start transmission (0x02, aka STX)
	 * - 4 bytes:	length
	 * - 1 byte:	checksum
	 * - 1 byte:	sequence number
	 * - 1 byte:	0x72 (for ijs) or 0x7d (for ijb)
	 * - ? bytes:	content of file
	 * - 1 byte:	end transmission (0x03, aka ETX)
	 */

	BinaryProtocol bp;
	bp.create(is_ijs ? BinaryProtocol::ECode::kSendIjsFile : BinaryProtocol::ECode::kSendIjbFile);
	bp.append(file_contents);

	write_and_expect_ack(bp);

	return *this;
}


BinaryProtocolConnection &BinaryProtocolConnection::send_eof(void)
{
	BinaryProtocol bp;
	bp.create(BinaryProtocol::ECode::kDoneWithFiles);

	write_and_expect_ack(bp);

	return *this;
}


BinaryProtocolConnection &BinaryProtocolConnection::write_and_expect_ack(const BinaryProtocol &bp)
{
	// before we attempt to write a new command, flush and disregard anything waiting to be read
	#if 0
	while (true)
	{
		const int rc = sock.waitUntilReady(true, 0);
		if (rc < 1)
		{
			// we have nothing waiting to be read
			break;
		}
		VBytes v(1000, '\0');
		const int bytes_read = sock.read(v.data(), v.size(), false);
		LOG_MSG("noticed there were " << bytes_read << " bytes waiting to be read");
		BinaryProtocol bp(v, bytes_read);
		LOG_MSG("previous binary protocol message purged");
	}
	#endif

	LOG_MSG("writing " << bp.content.size() << " bytes: " << bp.describe());
	const size_t bytes_written = sock.write(bp.content.data(), bp.content.size());
	LOG_MSG("number of bytes written to " << name << ": " << bytes_written);

	if (bytes_written != bp.content.size())
	{
		/// @throw Lox::Exception if we fail to write to the socket
		throw Lox::Exception(LOX_WHERE, "Failed to write the required number of bytes to the IncJet device at " + name + ".");
	}

	// now we expect the device to reply with a 0x09 (acknowledge)
	VBytes v(1000, '\0');
	const int bytes_read = sock.read(v.data(), v.size(), false);
	LOG_MSG(bytes_read << " bytes read");

	BinaryProtocol reply(v, bytes_read);
	#if 0
	if (reply.code != BinaryProtocol::ECode::kAcknowledgeMsg)
	{
		/// @throw Lox::Exception unexpected reply from device
		throw Lox::Exception(LOX_WHERE, "Unexpected reply from the IncJet device at " + name + ".");
	}

	/* ACK reply has several fields:
	 *
	 * 1 byte:	message id (id of message being ack'd)
	 * 1 byte:	ack sequence number
	 * 4 bytes:	encoder rate
	 * 4 bytes:	response code
	 * 4 bytes:	encoder count
	 */
	const VBytes msg = reply.message_data();
	if (msg.size() >= 14)
	{
		const uint8_t original_message_id	= msg.data()[0];
		const uint32_t *response_code		= reinterpret_cast<const uint32_t*>(msg.data() + 6);

		if (original_message_id != (uint8_t)bp.code)
		{
			/// @throw Lox::Exception unexpected ack from device
			throw Lox::Exception(LOX_WHERE, "Unexpected ACK message id from the IncJet device at " + name + ".");
		}

		if (*response_code != 0)
		{
			/// @throw Lox::Exception unexpected ack from device
			throw Lox::Exception(LOX_WHERE, "Unexpected ACK response code from the IncJet device at " + name + ".");
		}
	}
	#endif

	return *this;
}
