//..............................................................................
//
// Tibbo Modbus gateway monitor session
//

import "doc_Session.jnc"
import "io_base.jncx"
import "io_Socket.jnc"
import "iox_HostNameResolver.jnc"
import "ui_History.jnc"
import "ui_SocketUi.jnc"
import "ui_StdSessionInfoSet.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"

using extension ui.ComboBoxHistory;

//..............................................................................

class ModbusGatewayMonSession: doc.Session {
protected:
	enum State {
		Closed,
		Resolving,
		Opening,
		Connecting,
		Connected,
	}

	static char const* m_stateStringTable[] = {
		"Closed",
		"Resolving...",
		"Opening...",
		"Connecting...",
		"Connected",
	}

	enum ActionId {
		Connect,
		_Count,
	}

	enum IconId {
		Connect,
		Disconnect,
		_Count,
	}

	enum StatusPaneId {
		RemoteAddress,
		State,
		Temp,
		_Count,
	}

protected:

	// socket

	State bindable m_state;
	io.Socket m_socket;
	iox.HostNameResolver m_resolver;
	io.SocketAddress m_remoteAddress;
	uint_t m_syncId;

	// socket for Serching Tibbo controllers

	io.Socket m_soketSearch;

	// toolbar, status bar & menu

	ui.Action* m_actionTable[ActionId._Count];
	ui.Icon* m_iconTable[IconId._Count];
	ui.StatusPane* m_statusPaneTable[StatusPaneId._Count];
	ui.ComboBox* m_addressCombo;

	// property grid

	ui.SocketPropertySet m_socketPropertySet;
	ui.BufferPropertySet m_bufferPropertySet;

	// information grid

	ui.StdSessionInfoSet m_stdSessionInfoSet;
	ui.ThroughputInfoSet m_throughputInfoSet;
	ui.ChecksumInfoSet m_checksumInfoSet;

	// ui reactor

	reactor m_uiReactor;

	// values for start monitor session
	static char const* m_telnetCommand = "\x2Y\n\r";

	// Incoming data buffer
	static uint8_t m_bufferIncoming[512];
	size_t m_bufferIncomingLength;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode connect();
	bool errorcode connect(io.SocketAddress const* address);
	void disconnect();

	override void load(doc.Storage* storage);
	override void save(doc.Storage* storage);
	override void updateProperties();
	override bool errorcode applyProperties();
	override void restoreDefaultProperties();

protected:
	void handleConnectError(std.Error const* error);

	void onConnect();
	void onResolveCompleted();

	void onSocketEvent(
		uint_t syncId,
		io.SocketEvents triggeredEvents
	);

	void waitSocketEvents();

	void createUi();
}

// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

ModbusGatewayMonSession.construct(doc.PluginHost* pluginHost) {
	basetype.construct(pluginHost);
	m_resolver.construct(pluginHost);

	m_state = State.Closed;

	m_bufferIncomingLength = 0;

	createUi();

	pluginHost.m_log.addSelectionProcessor(log.ThroughputCalcRangeProcessor.factory~(m_throughputInfoSet));
	pluginHost.m_log.addSelectionProcessor(log.ChecksumCalcRangeProcessor.factory~(m_checksumInfoSet));
	m_resolver.m_onCompleted += onResolveCompleted;
}

void ModbusGatewayMonSession.createUi() {
	// toolbar

	m_pluginHost.m_toolBar.addLabel("Modbus Gateway Address:");
	m_addressCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_addressCombo.m_isEditable = true;
	m_addressCombo.m_onReturnPressed += connect;
	m_addressCombo.m_placeholderText = "Enter a remote IP or hostname...";

	m_iconTable[IconId.Connect] = new ui.Icon("images/connect.png");
	m_iconTable[IconId.Disconnect] = new ui.Icon("images/disconnect.png");

	m_actionTable[ActionId.Connect] = createAction("Connect", m_iconTable[IconId.Connect]);
	m_actionTable[ActionId.Connect].m_onTriggered += onConnect;

	// status bar

	m_statusPaneTable[StatusPaneId.RemoteAddress] = m_pluginHost.m_statusBar.addPane(, 100);
	m_statusPaneTable[StatusPaneId.RemoteAddress].m_color = ui.StdColor.Blue;
	m_statusPaneTable[StatusPaneId.State] = m_pluginHost.m_statusBar.addPane(, 80);
	m_statusPaneTable[StatusPaneId.Temp] = m_pluginHost.m_statusBar.addPane(, 100);

	// property grid

	ui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Connection",
		"TCP-specific connection settings"
	);

	m_socketPropertySet.m_defaultRemotePort = 23;
	//ui.SocketPropertySet.PropertyId mask = ui.SocketPropertySet.PropertyId.AllTcpClient & ~ui.SocketPropertySet.PropertyId.RemotePort;
	m_socketPropertySet.createProperties(m_pluginHost.m_propertyGrid, group); //,, mask);

	group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Buffering",
		"Advanced buffering options affecting throughtput and compatibility"
	);

	m_bufferPropertySet.createProperties(m_pluginHost.m_propertyGrid, group);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("Modbus gateway");
	m_stdSessionInfoSet.create(m_pluginHost.m_txRxStats, m_pluginHost.m_informationGrid, infoGroup);

	m_throughputInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);
	m_checksumInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);

	m_pluginHost.m_propertyGrid.resizeNameColumnToContents();
	m_pluginHost.m_informationGrid.resizeNameColumnToContents();

	m_uiReactor.start();
}

void ModbusGatewayMonSession.onConnect() {
	if (m_state)
		disconnect();
	else
		try connect();
}

bool errorcode ModbusGatewayMonSession.connect() {
	disconnect();

	startSession();

	char const* addressString = m_addressCombo.m_editText;
	m_addressCombo.addEditTextToHistory();

	io.SocketAddress address;
	bool isDirectAddress = try address.parse(addressString);
	if (isDirectAddress) {
		if (address.m_port == 0)
			address.m_port = m_socketPropertySet.m_remotePortProp.m_value;

		return try connect(&address);
	}

	m_state = State.Resolving;

	io.AddressFamily addressFamily = io.AddressFamily.Undefined;
	if (m_socketPropertySet.m_useLocalAddressProp.m_value) {
		io.SocketAddress const* adapterAddress = m_socketPropertySet.m_adapterProp.m_value;
		if (adapterAddress)
			addressFamily = adapterAddress.m_family;
	}

	m_resolver.m_defaultPort = m_socketPropertySet.m_remotePortProp.m_value;
	m_resolver.resolve(addressString, addressFamily);
	return true;

catch:
	handleConnectError(std.getLastError());
	return false;
}

bool errorcode ModbusGatewayMonSession.connect(io.SocketAddress const* remoteAddress) {
	disconnect();

	io.SocketAddress localAddress;

nestedscope:
	m_socket.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_socket.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_socket.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_socket.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
	m_socket.m_options = ui.getSocketOptions(m_socketPropertySet, m_bufferPropertySet);

	if (!m_socketPropertySet.m_useLocalAddressProp.m_value) {
		m_socket.open(remoteAddress.m_family, io.Protocol.Tcp);
	} else {
		io.SocketAddress const* adapterAddress = m_socketPropertySet.m_adapterProp.m_value;
		if (adapterAddress)
			localAddress = *adapterAddress;
		else
			localAddress.m_family = remoteAddress.m_family;

		localAddress.m_port = (uint16_t)m_socketPropertySet.m_localPortProp.m_value;

		m_socket.open(io.Protocol.Tcp, &localAddress);
	}

	m_remoteAddress = *remoteAddress;
	m_state = State.Connecting;
	m_socket.connect(remoteAddress);

	localAddress = m_socket.m_address;

	char const* p = $"$(*remoteAddress; B)$(localAddress; B)";
	m_logWriter.write(SocketLogRecordCode.ConnectingFrom, p, dynamic sizeof(p));

	waitSocketEvents();
	return true;

catch:
	if (!m_socketPropertySet.m_useLocalAddressProp.m_value) {
		m_logWriter.write(SocketLogRecordCode.Connecting, remoteAddress, sizeof(io.SocketAddress));
	} else {
		char const* p = $"$(*remoteAddress; B)$(localAddress; B)";
		m_logWriter.write(SocketLogRecordCode.ConnectingFrom, p, dynamic sizeof(p));
	}

	handleConnectError(std.getLastError());
	return false;
}

void ModbusGatewayMonSession.handleConnectError(std.Error const* error) {
	m_logWriter.write(SocketLogRecordCode.ConnectError, error);
	m_socket.close();
	m_state = State.Closed;
}

void ModbusGatewayMonSession.disconnect() {
	switch (m_state) {
	case State.Closed:
		return;

	case State.Resolving:
	case State.Connecting:
		m_logWriter.write(SocketLogRecordCode.ConnectCancelled);
		break;

	case State.Connected:
		TcpDisconnectLogRecordParams params;
		params.m_address = m_remoteAddress;
		params.m_flags = 0;

		if (m_socketPropertySet.m_tcpResetProp.m_value)
			params.m_flags |= TcpDisconnectLogRecordFlags.Reset;

		m_logWriter.write(
			SocketLogRecordCode.Disconnected,
			&params,
			sizeof(params)
		);

		m_isTransmitEnabled = false;
		m_stdSessionInfoSet.endSession();
		break;
	}

	m_socket.close();
	m_state = State.Closed;
	m_bufferIncomingLength = 0;
}

void ModbusGatewayMonSession.load(doc.Storage* storage) {
	m_addressCombo.loadHistory(storage, "addressHistory");
	m_addressCombo.m_editText = storage.readString("address");

	m_socketPropertySet.load(storage);
	m_bufferPropertySet.load(storage);
}

void ModbusGatewayMonSession.save(doc.Storage* storage) {
	m_socketPropertySet.save(storage);
	m_bufferPropertySet.save(storage);

	m_addressCombo.saveHistory(storage, "addressHistory");
	storage.writeString("address", m_addressCombo.m_editText); // overwrite
}

void ModbusGatewayMonSession.updateProperties() {
	m_socketPropertySet.m_remoteAddressProp.m_value = m_addressCombo.m_editText;
}

bool errorcode ModbusGatewayMonSession.applyProperties() {
	m_addressCombo.m_editText = m_socketPropertySet.m_remoteAddressProp.m_value;

	if (!m_socket.m_isOpen)
		return true;

	m_socket.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_socket.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_socket.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_socket.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
	m_socket.m_options = ui.getSocketOptions(m_socketPropertySet, m_bufferPropertySet);
	return true;
}

void ModbusGatewayMonSession.restoreDefaultProperties() {
	m_socketPropertySet.restoreDefaults();
	m_socketPropertySet.m_remotePortProp.m_value = 23;
	m_bufferPropertySet.restoreDefaults();
}

void ModbusGatewayMonSession.waitSocketEvents() {
	io.SocketEvents eventMask = io.SocketEvents.IoError;

	switch (m_state) {
	case State.Connecting:
		eventMask |= io.SocketEvents.TcpConnected;
		break;

	case State.Connected:
		eventMask |=
			io.SocketEvents.IncomingData |
			io.SocketEvents.TcpDisconnected |
			io.SocketEvents.TcpReset;

		break;
	}

	m_socket.wait(eventMask, onSocketEvent~(++m_syncId) @ m_pluginHost.m_mainThreadScheduler);
}

void ModbusGatewayMonSession.onSocketEvent(
	uint_t syncId,
	io.SocketEvents triggeredEvents
) {
	if (syncId != m_syncId)
		return;

	if (triggeredEvents & io.SocketEvents.IoError) {
		handleConnectError(m_socket.m_ioError);
		return;
	}

	if (triggeredEvents & io.SocketEvents.TcpConnected) {
		m_logWriter.write(SocketLogRecordCode.ConnectCompleted);
		m_state = State.Connected;
		m_isTransmitEnabled = true;
		m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);

		size_t sz = m_socket.write(m_telnetCommand, 4);
		if (sz != 4) {
			m_logWriter.write(ModbusGatewayMonLogRecordCode.CommandError);
		}
	}

	if (triggeredEvents & io.SocketEvents.TcpDisconnected) {
		for (;;) { // read all the buffered data
			size_t result = m_socket.read(m_bufferIncoming, sizeof(m_bufferIncoming));
			if (!result)
				break;

			m_logWriter.write(log.StdRecordCode.Rx, m_bufferIncoming, result);
		}

		TcpDisconnectLogRecordParams recordParams;
		recordParams.m_address = m_remoteAddress;
		recordParams.m_flags = TcpDisconnectLogRecordFlags.Remote;

		if (triggeredEvents & io.SocketEvents.TcpReset)
			recordParams.m_flags |= TcpDisconnectLogRecordFlags.Reset;

		m_logWriter.write(
			SocketLogRecordCode.Disconnected,
			&recordParams,
			sizeof(recordParams)
		);

		m_socket.close();
		m_state = State.Closed;
		m_isTransmitEnabled = false;
		m_stdSessionInfoSet.endSession();
		return;
	}

	if ((triggeredEvents & io.SocketEvents.ReadBufferFull) &&
		m_bufferPropertySet.m_notifyReadBufferFullProp.m_value)
		m_logWriter.write(log.StdRecordCode.RxBufferFull);

	if (triggeredEvents & io.SocketEvents.IncomingData) {
		size_t result;
		bool bigdata = true;

		while (bigdata == true) {
			result = try m_socket.read(&m_bufferIncoming + m_bufferIncomingLength, sizeof(m_bufferIncoming) - m_bufferIncomingLength);
			if (result == -1) {
				bigdata = false;
			} else {
				if (result == sizeof(m_bufferIncoming) - m_bufferIncomingLength) bigdata = true;
				else bigdata = false;
				m_bufferIncomingLength += result;
				char const* str = $"Data received. Length: $result All data length: $m_bufferIncomingLength";
				//writeLog (ModbusGatewayMonLogRecordCode.Log, str, dynamic sizeof(str));
				//writeLog (log.StdRecordCode.Rx, m_bufferIncoming, 64);
			}

			while (m_bufferIncomingLength > 0) {
				if (m_bufferIncomingLength >= 3) {
					if ((m_bufferIncoming[0] == 0x02) && (m_bufferIncoming[1] == 0x41)) { // Telnet command responce 0x02 0x41 0x0d
						//char const* str = "Command send complete";
						//writeLog (ModbusGatewayMonLogRecordCode.Log, str, dynamic sizeof(str));
						m_bufferIncomingLength -= 3;
						if (m_bufferIncomingLength > 0)
							memcpy(&m_bufferIncoming, &m_bufferIncoming[3], m_bufferIncomingLength);
					}
				}
				if (m_bufferIncomingLength >= 4) {
					if ((m_bufferIncoming[0] == 0xff) && (m_bufferIncoming[1] == 0x00)) {
						uint16_t sizePacket = (uint16_t)m_bufferIncoming[2];
						if (sizePacket == 0 || sizePacket > 300) {
							m_bufferIncomingLength = 0;
							//char const* str = $"sizePacket=$sizePacket";
							//writeLog (ModbusGatewayMonLogRecordCode.Log, str, dynamic sizeof(str));
						} else
							if (sizePacket + 2 <= m_bufferIncomingLength) {
								// Packet receiving complete
								m_logWriter.write(ModbusGatewayMonLogRecordCode.DumpPacket, &m_bufferIncoming[4], sizePacket - 2);
								//writeLog (log.StdRecordCode.Rx, m_bufferIncoming, 64);
								m_bufferIncomingLength -= sizePacket + 2;
								if (m_bufferIncomingLength > 0)
									memcpy(&m_bufferIncoming, &m_bufferIncoming[sizePacket + 2], m_bufferIncomingLength);
								//writeLog (log.StdRecordCode.Rx, m_bufferIncoming, 64);
							}
					} else {    // Wrong Header
						uint8_t _buf0 = m_bufferIncoming[0];
						char const* str = $"Wrong packet header. Length:$m_bufferIncomingLength [0]:$_buf0";
						m_logWriter.write(ModbusGatewayMonLogRecordCode.Log, str, dynamic sizeof(str));
						m_logWriter.write(log.StdRecordCode.Rx, m_bufferIncoming, 128);
						m_bufferIncomingLength = 0;
					}
				}
			}
		}
	}

	waitSocketEvents();
}

void ModbusGatewayMonSession.onResolveCompleted() {
	m_state = State.Closed;

	if (m_resolver.m_state == iox.HostNameResolver.State.Resolved)
		try connect(m_resolver.m_address);
}

reactor ModbusGatewayMonSession.m_uiReactor {
	m_title = $"Modbus $(m_addressCombo.m_editText)";
	m_actionTable[ActionId.Connect].m_text = m_state ? "Disconnect" : "Connect";
	m_actionTable[ActionId.Connect].m_icon = m_iconTable[m_state ? IconId.Disconnect : IconId.Connect];
	m_statusPaneTable[StatusPaneId.State].m_text = m_stateStringTable[m_state];
	m_statusPaneTable[StatusPaneId.RemoteAddress].m_text = m_state > State.Resolving ? m_remoteAddress.getString() : "<peer-address>";
	m_statusPaneTable[StatusPaneId.RemoteAddress].m_isVisible = m_state > State.Resolving;
}

//..............................................................................
