//..............................................................................
//
// UDP 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_BufferPropertySet.jnc"
import "ui_StdSessionInfoSet.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"
import "std_Buffer.jnc"

using extension ui.ComboBoxHistory;

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

class UdpSession: doc.Session {
protected:
	enum Defaults {
		RemotePort = 65535,
	}

	static char DefaultRemoteAddress[] = "192.168.1.255";

	enum State {
		Closed,
		Opened,
	}

	enum ActionId {
		Open,
		_Count,
	}

	enum IconId {
		Open,
		Close,
		Compass,
		_Count,
	}

	enum StatusPaneId {
		State,
		RemoteAddress,
		_Count,
	}

protected:
	// socket

	State bindable m_state;
	io.Socket m_socket;
	iox.HostNameResolver m_resolver;
	std.Buffer m_readBuffer;
	std.Buffer m_pendingDatagram;
	bool m_hasPendingDatagram;
	uint_t m_socketSyncId;

	// 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_adapterCombo;
	ui.ComboBox* m_localPortCombo;
	ui.ComboBox* m_remoteAddressCombo;

	// property grid

	ui.BoolProperty* m_autoRemoteAddressProp;
	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;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode openSocket();
	void closeSocket();

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

	override size_t errorcode transmit(
		void const* p,
		size_t size
	);

	override bool errorcode connectSession() {
		return m_sessionConnectionState ? true : openSocket();
	}

	override void disconnectSession() {
		closeSocket();
	}

	override bool errorcode dispatch(
		uint_t code,
		variant in,
		variant* out
	);

protected:
	void onOpen() {
		m_state ? closeSocket() : try openSocket();
	}

	void onPortComboEnter() {
		try openSocket();
	}

	void onRemoteAddressChanged() {
		m_resolver.cancel();
	}

	void onRemoteAddressEnter() {
		try resolve();
	}

	void onResolveCompleted();

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

	void createUi();
	bool errorcode resolve();
	void waitSocketEvents();

	size_t writeDatagram(
		void const* p,
		size_t size,
		io.SocketAddress const* address
	);
}

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

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

	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 UdpSession.createUi() {
	// toolbar

	m_iconTable[IconId.Open] = new ui.Icon("images/open.png");
	m_iconTable[IconId.Close] = new ui.Icon("images/close.png");
	m_iconTable[IconId.Compass] = new ui.Icon("images/compass.png");

	m_pluginHost.m_toolBar.addLabel("Remote:");
	m_remoteAddressCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_remoteAddressCombo.m_isEditable = true;
	m_remoteAddressCombo.m_onEnter += onRemoteAddressEnter;
	bindingof(m_remoteAddressCombo.m_editText) += onRemoteAddressChanged;
	m_remoteAddressCombo.m_placeholderText = "Enter a remote IP or hostname...";

	m_pluginHost.m_toolBar.addLabel("Adapter:");
	m_adapterCombo = ui.createNetworkAdapterComboBox(m_pluginHost.m_toolBar, ui.NetworkAdapterUsage.Listener);

	m_pluginHost.m_toolBar.addLabel("Local port:");
	m_localPortCombo = m_pluginHost.m_toolBar.addComboBox();
	m_localPortCombo.m_isEditable = true;
	m_localPortCombo.m_onEnter += onPortComboEnter;

	m_actionTable[ActionId.Open] = createAction("Open socket", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Open].m_onTriggered += onOpen;

	// status bar

	m_statusPaneTable[StatusPaneId.RemoteAddress] = m_pluginHost.m_statusBar.addPane(, 80);
	m_statusPaneTable[StatusPaneId.State] = m_pluginHost.m_statusBar.addPane(, 80);

	// property grid

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

	m_socketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		group,,
		ui.SocketPropertySet.PropertyId.AllUdpClient
	);

	m_autoRemoteAddressProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,
		m_socketPropertySet.m_adapterProp,
		"Auto-switch remote address",
		"Automatically switch remote address upon receiving a datagram"
	);

	m_socketPropertySet.m_defaultRemotePort = Defaults.RemotePort;

	m_bufferPropertySet.createPropertiesGrouped(
		m_pluginHost.m_propertyGrid,,,
		ui.BufferPropertySet.PropertyId.All & ~(
			ui.BufferPropertySet.PropertyId.KeepReadBlockSize |
			ui.BufferPropertySet.PropertyId.KeepWriteBlockSize
		)
	);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("UDP socket");

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

bool errorcode UdpSession.openSocket() {
	io.SocketAddress address;

nestedscope:
	closeSocket();
	startSession();

	char const* portString = m_localPortCombo.m_editText;
	m_localPortCombo.addEditTextToHistory();

	address = *(io.SocketAddress const*) m_adapterCombo.m_currentData;
	address.m_port = (uint16_t)atoi(portString);

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

	m_socket.open(io.Protocol.Udp, &address);
	address = m_socket.m_address;
	m_logWriter.write(SocketLogRecordCode.Open, &address, sizeof(address));
	m_state = State.Opened;
	m_sessionConnectionState = SessionConnectionState.Connected;
	waitSocketEvents();
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	return true;

catch:
	std.Error const* error = std.getLastError();
	m_logWriter.write(SocketLogRecordCode.OpenError, address, sizeof(address), error, error.m_size);
	return false;
}

void UdpSession.closeSocket() {
	if (m_state == State.Closed)
		return;

	io.SocketAddress address = m_socket.m_address;
	m_socket.close();
	m_resolver.cancel();
	m_logWriter.write(SocketLogRecordCode.Close, &address, sizeof(address));
	m_state = State.Closed;
	m_sessionConnectionState = SessionConnectionState.Disconnected;
	m_socketSyncId++;
	m_stdSessionInfoSet.endSession();
}

void UdpSession.waitSocketEvents() {
	io.SocketEvents eventMask =
		io.SocketEvents.IoError |
		io.SocketEvents.IncomingData |
		io.SocketEvents.ReadBufferFull;

	if (!m_isTransmitEnabled)
		eventMask |= io.SocketEvents.WriteBufferReady;

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

void UdpSession.load(doc.Storage* storage) {
	m_remoteAddressCombo.loadHistory(storage, "remoteAddressHistory");
	m_localPortCombo.loadHistory(storage, "localPortHistory");

	m_remoteAddressCombo.m_editText = storage.readString("remoteAddress", DefaultRemoteAddress);
	m_autoRemoteAddressProp.m_value = storage.readBool("autoRemoteAddress");
	m_adapterCombo.m_currentIndex = storage.readInt("adapterIdx");
	m_localPortCombo.m_editText = storage.readString("localPort");

	m_socketPropertySet.load(storage);
	m_bufferPropertySet.load(storage);
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

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

	m_remoteAddressCombo.saveHistory(storage, "remoteAddressHistory");
	m_localPortCombo.saveHistory(storage, "localPortHistory");

	storage.writeString("remoteAddress", m_remoteAddressCombo.m_editText);
	storage.writeInt("autoRemoteAddress", m_autoRemoteAddressProp.m_value);
	storage.writeInt("adapterIdx", (int) m_adapterCombo.m_currentIndex);
	storage.writeString("localPort", m_localPortCombo.m_editText);
}

void UdpSession.updateProperties() {
	m_socketPropertySet.m_remoteAddressProp.m_value = m_remoteAddressCombo.m_editText;
	m_socketPropertySet.m_adapterProp.m_currentIndex = m_adapterCombo.m_currentIndex;
	m_socketPropertySet.m_localPortProp.m_value = atoi(m_localPortCombo.m_editText);
}

bool errorcode UdpSession.applyProperties() {
	m_remoteAddressCombo.m_editText = m_socketPropertySet.m_remoteAddressProp.m_value;
	m_adapterCombo.m_currentIndex = m_socketPropertySet.m_adapterProp.m_currentIndex;
	m_localPortCombo.m_editText = $"$(m_socketPropertySet.m_localPortProp.m_value)";
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.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 UdpSession.restoreDefaultProperties() {
	m_socketPropertySet.restoreDefaults();
	m_bufferPropertySet.restoreDefaults();
	m_autoRemoteAddressProp.m_value = false;
}

size_t errorcode UdpSession.transmit(
	void const* p,
	size_t size
) {
	if (m_state == State.Closed) {
		std.setError("UDP socket not opened");
		return -1;
	}

	if (m_resolver.m_state == iox.HostNameResolver.State.Idle ||
		m_resolver.m_state == iox.HostNameResolver.State.Error)
		resolve(); // and then inspect resolver state again

	switch (m_resolver.m_state) {
	case iox.HostNameResolver.State.Resolved:
		break;

	case iox.HostNameResolver.State.Resolving:
		m_pendingDatagram.copy(p, size);
		m_hasPendingDatagram = true;
		return size;

	default:
		assert(false);
	}

	return writeDatagram(p, size, &m_resolver.m_address);
}

bool errorcode UdpSession.dispatch(
	uint_t code,
	variant in,
	variant* out
) {
	switch (code) {
	case UdpDispatchCode.GetRemoteAddress:
		*out = m_remoteAddressCombo.m_currentText;
		break;

	case UdpDispatchCode.SetRemoteAddress:
		m_remoteAddressCombo.m_editText = in;
		break;

	case UdpDispatchCode.GetLocalPort:
		*out = atoi(m_localPortCombo.m_currentText);
		break;

	case UdpDispatchCode.SetLocalPort:
		m_localPortCombo.m_editText = $"%d"((uint_t)in);
		break;

	case UdpDispatchCode.GetRemotePort:
		*out = m_socketPropertySet.m_remotePortProp.m_value;
		break;

	case UdpDispatchCode.SetRemotePort:
		m_socketPropertySet.m_remotePortProp.m_value = in;
		break;

	case UdpDispatchCode.Open:
		return openSocket();

	case UdpDispatchCode.Close:
		closeSocket();
		break;
	}

	return true;
}

bool errorcode UdpSession.resolve() {
	m_resolver.cancel();
	m_remoteAddressCombo.addEditTextToHistory();
	char const* addressString = m_remoteAddressCombo.m_editText;
	m_resolver.m_defaultPort = m_socketPropertySet.m_remotePortProp.m_value;
	return m_resolver.resolve(addressString);
}

size_t UdpSession.writeDatagram(
	void const* p,
	size_t size,
	io.SocketAddress const* address
) {
	size_t result = m_socket.writeDatagram(p, size, address);

	m_logWriter.write(
		UdpLogRecordCode.TxPacketEx,
		&result,
		sizeof(uint32_t),
		address,
		sizeof(io.SocketAddress)
	);

	if (result)
		m_logWriter.write(log.StdRecordCode.Tx, p, result);

	return result;
}

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

	if (triggeredEvents & io.SocketEvents.IoError) {
		m_logWriter.write(log.StdRecordCode.Error, m_socket.m_ioError);
		m_socket.close();
		m_state = State.Closed;
		m_sessionConnectionState = SessionConnectionState.Disconnected;
		m_isTransmitEnabled = false;
		m_stdSessionInfoSet.endSession();
		return;
	}

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

	if (triggeredEvents & io.SocketEvents.IncomingData) {
		io.SocketAddress remoteAddress;
		size_t result = m_socket.readDatagram(m_readBuffer.m_p, m_readBuffer.m_size, &remoteAddress);

		if (m_autoRemoteAddressProp.m_value)
			m_remoteAddressCombo.m_editText = remoteAddress.getString();

		m_logWriter.write(
			UdpLogRecordCode.RxPacketEx,
			&result,
			sizeof(uint32_t),
			&remoteAddress,
			sizeof(io.SocketAddress)
		);

		if (result)
			m_logWriter.write(log.StdRecordCode.Rx, m_readBuffer.m_p, result);
	}

	if (triggeredEvents & io.SocketEvents.WriteBufferReady)
		m_isTransmitEnabled = true;

	waitSocketEvents();
}

void UdpSession.onResolveCompleted() {
	if (m_resolver.m_state == iox.HostNameResolver.State.Resolved && m_hasPendingDatagram)
		writeDatagram(m_pendingDatagram.m_p, m_pendingDatagram.m_size, &m_resolver.m_address);

	m_hasPendingDatagram = false;
}

reactor UdpSession.m_uiReactor {
	m_title = $"UDP @ $(m_localPortCombo.m_editText) \u2192 $(m_remoteAddressCombo.m_editText)";
	m_isTransmitEnabled = m_state == State.Opened;
	m_actionTable[ActionId.Open].m_text = m_state ? "Close Socket" : "Open Socket";
	m_actionTable[ActionId.Open].m_icon = m_iconTable[m_state ? IconId.Close : IconId.Open];
	m_statusPaneTable[StatusPaneId.State].m_text = m_state ? "Opened" : "Closed";
	m_remoteAddressCombo.m_backgroundColor = m_autoRemoteAddressProp.m_value ? ui.StdColor.PastelGreen : ui.ColorFlags.Undefined;
	m_remoteAddressCombo.m_toolTipText = m_autoRemoteAddressProp.m_value ?
		"This address will automatically switch to the address of the last inbound datagram" :
		null;

	onevent bindingof(m_resolver.m_state)() {
		m_resolver.updateStatusPane(m_statusPaneTable[StatusPaneId.RemoteAddress]);
	}
}

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