//..............................................................................
//
// TCP Proxy session
//

import "io_base.jncx"
import "io_Socket.jnc"
import "io_SocketAddressResolver.jnc"
import "io_SocketUi.jnc"
import "io_BufferUi.jnc"
import "std_Buffer.jnc"
import "doc_Session.jnc"
import "gui_History.jnc"
import "gui_StdSessionInfo.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"

using extension gui.ComboBoxHistory;

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

class TcpProxySession: doc.Session
{
protected:
	enum Defaults
	{
		LocalPort           = 8080,
		TxDisconnectTimeout = 1000, // wait TX on disconnect for at most 1 sec
		OverrideConnection  = true,
	}

	enum ListenerState
	{
		Closed,
		Resolving,
		Listening,
	}

	static char const* m_listenerStateStringTable[] =
	{
		"Not listening",
		"Resolving...",
		"Listening",
	}

	enum ServerConnectionState
	{
		Closed,
		Connected,
	}

	enum ClientConnectionState
	{
		Closed,
		Connecting,
		Connected,
	}

	static char const* m_clientConnectionStateStringTable[] =
	{
		"Not connected",
		"Connecting...",
		"Connected",
	}

	enum ActionId
	{
		Listen,
		Disconnect,
		_Count,
	}

	enum IconId
	{
		Listen,
		StopListen,
		Disconnect,
		_Count,
	}

	enum StatusPaneId
	{
		ServerRemoteAddress,
		ListenerState,
		ClientConnectionState,
		_Count,
	}

protected:
	// server listener socket

	ListenerState bindable m_listenerState;
	io.Socket m_listenerSocket;
	io.SocketAddress m_localAddress;
	std.Buffer m_readBuffer;
	uint_t m_listenerSyncId;

	// server connection socket

	ServerConnectionState bindable m_serverConnectionState;
	io.Socket* m_serverConnectionSocket;
	io.SocketAddress m_serverRemoteAddress;
	std.Buffer m_serverPendingData;
	uint_t m_serverConnectionSyncId;

	// client connection socket

	ClientConnectionState bindable m_clientConnectionState;
	io.Socket m_clientConnectionSocket;
	io.SocketAddress m_clientRemoteAddress;
	uint_t m_clientConnectionSyncId;

	// resolver

	io.SocketAddressResolver m_resolver;
	uint_t m_resolverSyncId;
	bool m_isTargetAddressResolved;
	bool m_wasTargetAddressChangedWhileResolving;

	// toolbar, status bar & menu

	gui.Action* m_actionTable[ActionId._Count];
	gui.Icon* m_iconTable[IconId._Count];
	gui.StatusPane* m_statusPaneTable[StatusPaneId._Count];
	gui.ComboBox* m_adapterCombo;
	gui.ComboBox* m_serverPortCombo;
	gui.ComboBox* m_clientAddressCombo;

	// property grid

	gui.BoolProperty* m_overrideConnectionProp;
	io.SocketPropertySet m_listenerSocketPropertySet;
	io.SocketPropertySet m_connectionSocketPropertySet;
	io.BufferPropertySet m_bufferPropertySet;

	// information grid

	gui.StdSessionInfoSet m_stdSessionInfoSet;
	log.ThroughputInfoSet m_throughputInfoSet;
	log.ChecksumInfoSet m_checksumInfoSet;

	// ui reactor

	reactor m_uiReactor;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode listen();
	stopListen();
	disconnect();

	bool connectTarget();
	disconnectTarget();

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

protected:
	onListen();
	onTargetAddressChanged();

	onListenerSocketEvent(
		uint_t syncId,
		io.SocketEvents triggeredEvents
		);

	onServerConnectionSocketEvent(
		uint_t syncId,
		io.SocketEvents triggeredEvents
		);

	onClientConnectionSocketEvent(
		uint_t syncId,
		io.SocketEvents triggeredEvents
		);

	onResolveCompleted(
		uint_t syncId,
		io.SocketAddress const* addressTable,
		size_t addressCount,
		std.Error const* error
		);

	createUi();
	listenImpl();
	handleListenError(std.Error const* error);
	handleConnectError(std.Error const* error);
	waitListenerSocketEvents();
	waitServerConnectionSocketEvents();
	waitClientConnectionSocketEvents();
	size_t errorcode forwardDataFromClientToServer();
	size_t errorcode forwardDataFromServerToClient();
}

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

TcpProxySession.construct(doc.PluginHost* pluginHost)
{
	basetype.construct(pluginHost);

	createUi();

	pluginHost.m_log.addSelectionProcessor(log.ThroughputCalcRangeProcessor.factory ~(m_throughputInfoSet));
	pluginHost.m_log.addSelectionProcessor(log.ChecksumCalcRangeProcessor.factory ~(m_checksumInfoSet));
	m_stdSessionInfoSet.addLogListener(pluginHost.m_log);
}

TcpProxySession.createUi()
{
	// toolbar

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

	m_pluginHost.m_toolBar.addLabel("Port:");
	m_serverPortCombo = m_pluginHost.m_toolBar.addComboBox();
	m_serverPortCombo.m_isEditable = true;
	m_serverPortCombo.m_editText = "1001";

	m_pluginHost.m_toolBar.addLabel("Remote:");
	m_clientAddressCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_clientAddressCombo.m_isEditable = true;
	m_clientAddressCombo.m_editText = "127.0.0.1:1002";
	m_clientAddressCombo.m_onChanged += onTargetAddressChanged;

	m_iconTable[IconId.Listen] = new gui.Icon("images/listen.png");
	m_iconTable[IconId.StopListen] = new gui.Icon("images/pause.png");
	m_iconTable[IconId.Disconnect] = new gui.Icon("images/disconnect.png");

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

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

	// status bar

	m_statusPaneTable[StatusPaneId.ServerRemoteAddress] = m_pluginHost.m_statusBar.addPane(, 100);
	m_statusPaneTable[StatusPaneId.ServerRemoteAddress].m_color = gui.StdColor.Blue;
	m_statusPaneTable[StatusPaneId.ListenerState] = m_pluginHost.m_statusBar.addPane(, 80);
	m_statusPaneTable[StatusPaneId.ClientConnectionState] = m_pluginHost.m_statusBar.addPane(, 80);

	// property grid

	gui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Server",
		"TCP server settings"
		);

	m_listenerSocketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		group,,
		io.SocketPropertySet.PropertyId.ServerAdapter |
		io.SocketPropertySet.PropertyId.LocalPort |
		io.SocketPropertySet.PropertyId.ReuseLocalAddress
		);

	m_listenerSocketPropertySet.m_defaultLocalPort = Defaults.LocalPort;

	m_overrideConnectionProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Override Connections",
		"Accept incoming connection even if already connected"
		);

	group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Connection",
		"TCP connection settings"
		);

	m_connectionSocketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		group,,
		io.SocketPropertySet.PropertyId.RemoteAddress |
		io.SocketPropertySet.PropertyId.RemotePort |
		io.SocketPropertySet.PropertyId.TcpKeepAlive |
		io.SocketPropertySet.PropertyId.TcpNagle |
		io.SocketPropertySet.PropertyId.TcpReset
		);

	m_bufferPropertySet.createPropertiesGrouped(m_pluginHost.m_propertyGrid);

	// information grid

	gui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("TCP proxy");

	m_stdSessionInfoSet.createInformationValues(m_pluginHost.m_informationGrid, infoGroup);
	m_throughputInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);
	m_checksumInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);

	m_pluginHost.m_informationGrid.resizeNameColumnToContents();

	m_uiReactor.start();
}

TcpProxySession.onListen()
{
	if (m_listenerState)
		stopListen();
	else
		try listen();
}

bool errorcode TcpProxySession.listen()
{
	stopListen();
	startSession();

	m_serverPortCombo.addEditTextToHistory();
	m_clientAddressCombo.addEditTextToHistory();

	char const* targetAddressString = m_clientAddressCombo.m_editText;

	io.SocketAddress targetAddress;
	bool isDirectAddress = try targetAddress.parse(targetAddressString);
	if (isDirectAddress || m_isTargetAddressResolved)
	{
		if (isDirectAddress)
		{
			if (!targetAddress.m_ip4.m_port) // IPv4 & IPv6 share the same port location
				targetAddress.m_ip4.m_port = m_connectionSocketPropertySet.m_remotePortProp.m_value;

			m_clientRemoteAddress = targetAddress;
		}

		listenImpl();
	}
	else
	{
		writeLog(
			SocketLogRecordCode.Resolving,
			targetAddressString,
			strlen(targetAddressString)
			);

		m_resolver.resolve(targetAddressString, onResolveCompleted ~(++m_resolverSyncId) @ m_pluginHost.m_mainThreadScheduler);

		m_listenerState = ListenerState.Resolving;
		m_isTargetAddressResolved = false;
		m_wasTargetAddressChangedWhileResolving = false;
	}

	return true;

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

TcpProxySession.listenImpl()
{
	io.SocketAddress address = *(io.SocketAddress const*) m_adapterCombo.m_currentData;
	address.m_ip4.m_port = (uint16_t)atoi(m_serverPortCombo.m_editText); // same for IPv4/IPv6

	m_listenerSocket.open(io.Protocol.Tcp, &address);
	m_listenerSocket.listen();

	writeLog(
		SocketLogRecordCode.Listening,
		&address,
		sizeof(address)
		);

	m_localAddress = address;
	m_listenerState = ListenerState.Listening;
	waitListenerSocketEvents();
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);

catch:
	handleListenError(std.getLastError());
}

TcpProxySession.stopListen()
{
	m_serverPortCombo.m_backgroundColor = gui.StdColor.White;
	m_clientAddressCombo.m_backgroundColor = gui.StdColor.White;

	switch (m_listenerState)
	{
		case ListenerState.Closed:
			return;

		case ListenerState.Resolving:
			writeLog(SocketLogRecordCode.ResolveCancelled);
			m_resolverSyncId++;
			break;

		case ListenerState.Listening:
			writeLog(
				SocketLogRecordCode.ListenStopped,
				&m_localAddress,
				sizeof(m_localAddress)
				);
			break;
	}

	m_listenerSocket.close();
	m_listenerState = ListenerState.Closed;
	m_listenerSyncId++;
	m_resolverSyncId++;
	m_stdSessionInfoSet.endSession();
}

TcpProxySession.disconnect()
{
	disconnectTarget();

	if (m_serverConnectionState != ServerConnectionState.Connected)
		return;

	TcpDisconnectLogRecordParams params;
	params.m_address = m_serverRemoteAddress;

	writeLog(SocketLogRecordCode.Disconnected, &params, sizeof(params));

	m_serverConnectionSocket.close();
	m_serverConnectionSocket = null;
	m_serverConnectionState = ServerConnectionState.Closed;
	m_serverConnectionSyncId++;
}

bool TcpProxySession.connectTarget()
{
	disconnectTarget();

	writeLog(
		SocketLogRecordCode.Connecting,
		&m_clientRemoteAddress,
		sizeof(m_clientRemoteAddress)
		);

	m_clientConnectionSocket.open(m_clientRemoteAddress.m_family, io.Protocol.Tcp);
	m_clientConnectionSocket.connect(&m_clientRemoteAddress);

	m_clientConnectionState = ClientConnectionState.Connecting;
	waitClientConnectionSocketEvents();
	return true;

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

TcpProxySession.disconnectTarget()
{
	if (m_clientConnectionState != ClientConnectionState.Connected)
		return;

	TcpDisconnectLogRecordParams params;
	params.m_address = m_clientRemoteAddress;

	writeLog(SocketLogRecordCode.Disconnected, &params, sizeof(params));

	m_clientConnectionSocket.close();
	m_clientConnectionState = ClientConnectionState.Closed;
}

override void TcpProxySession.load(doc.Storage* storage)
{
	m_serverPortCombo.loadHistory(storage, "portHistory");
	m_clientAddressCombo.loadHistory(storage, "targetAddressHistory");
	m_adapterCombo.m_currentIndex  = storage.readInt("adapterIdx");
	m_serverPortCombo.m_editText = storage.readString("port");
	m_clientAddressCombo.m_editText = storage.readString("targetAddress");
	m_overrideConnectionProp.m_value = storage.readBool("overrideConnection", Defaults.OverrideConnection);

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

override void TcpProxySession.save(doc.Storage* storage)
{
	m_serverPortCombo.saveHistory(storage, "portHistory");
	m_clientAddressCombo.saveHistory(storage, "targetAddressHistory");

	m_listenerSocketPropertySet.save(storage);
	m_connectionSocketPropertySet.save(storage);
	m_bufferPropertySet.save(storage);

	storage.writeInt("adapterIdx", (int) m_adapterCombo.m_currentIndex);
	storage.writeString("port", m_serverPortCombo.m_editText);
	storage.writeString("targetAddress", m_clientAddressCombo.m_editText);
	storage.writeBool("overrideConnection", m_overrideConnectionProp.m_value);
}

override void TcpProxySession.updateProperties()
{
	m_listenerSocketPropertySet.m_adapterProp.m_currentIndex = m_adapterCombo.m_currentIndex;
	m_listenerSocketPropertySet.m_localPortProp.m_value = atoi(m_serverPortCombo.m_editText);
	m_connectionSocketPropertySet.m_remoteAddressProp.m_value = m_clientAddressCombo.m_editText;
}

override bool errorcode TcpProxySession.applyProperties()
{
	m_adapterCombo.m_currentIndex = m_listenerSocketPropertySet.m_adapterProp.m_currentIndex;
	m_serverPortCombo.m_editText = $"$(m_listenerSocketPropertySet.m_localPortProp.m_value)";
	m_clientAddressCombo.m_editText = m_connectionSocketPropertySet.m_remoteAddressProp.m_value;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
	return true;
}

override void TcpProxySession.restoreDefaultProperties()
{
	m_listenerSocketPropertySet.restoreDefaults();
	m_connectionSocketPropertySet.restoreDefaults();
	m_bufferPropertySet.restoreDefaults();
	m_overrideConnectionProp.m_value = Defaults.OverrideConnection;
}

TcpProxySession.onTargetAddressChanged()
{
	m_isTargetAddressResolved = false;

	if (m_listenerState == ListenerState.Resolving)
		m_wasTargetAddressChangedWhileResolving = true;
}

TcpProxySession.onListenerSocketEvent(
	uint_t syncId,
	io.SocketEvents triggeredEvents
	)
{
	if (syncId != m_listenerSyncId)
		return;

	if (triggeredEvents & io.SocketEvents.IoError)
	{
		writeLog(log.StdRecordCode.Error, m_listenerSocket.m_ioError, m_listenerSocket.m_ioError.m_size);
		m_listenerSocket.close();
		m_listenerState = ListenerState.Closed;
		m_stdSessionInfoSet.endSession();
		return;
	}

	if (triggeredEvents & io.SocketEvents.IncomingConnection)
	{
		io.SocketAddress address;
		io.Socket* socket = m_listenerSocket.accept(&address, true);

		if (m_serverConnectionState == ServerConnectionState.Connected && !m_overrideConnectionProp.m_value)
		{
			socket.close();
			writeLog(SocketLogRecordCode.ClientRejected, &address, sizeof(address));
			return;
		}

		socket.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
		socket.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
		socket.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
		socket.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
		socket.m_options = io.getSocketOptions(m_connectionSocketPropertySet, m_bufferPropertySet);
		socket.unsuspend();

		disconnect();

		writeLog(SocketLogRecordCode.ClientConnected, &address, sizeof(address));

		m_serverRemoteAddress = address;
		m_serverConnectionSocket = socket;
		m_serverConnectionState = ServerConnectionState.Connected;
		m_serverPendingData.clear();
		waitServerConnectionSocketEvents();

		bool result = connectTarget();
		if (!result)
			disconnect();
	}

	waitListenerSocketEvents();
}

TcpProxySession.onServerConnectionSocketEvent(
	uint_t syncId,
	io.SocketEvents triggeredEvents
	)
{
	if (syncId != m_serverConnectionSyncId)
		return;

	if (triggeredEvents & io.SocketEvents.IoError)
	{
		writeLog(log.StdRecordCode.Error, m_serverConnectionSocket.m_ioError, m_serverConnectionSocket.m_ioError.m_size);
		m_serverConnectionSocket.close();
		m_serverConnectionSocket = null;
		m_serverConnectionState = ServerConnectionState.Closed;
		disconnectTarget();
		return;
	}

	if (triggeredEvents & io.SocketEvents.Disconnected)
	{
		for (;;) // read all the buffered data
		{
			size_t result = forwardDataFromServerToClient();
			if (!result)
				break;
		}

		if (m_clientConnectionState == ClientConnectionState.Connected)
			m_clientConnectionSocket.blockingWait(io.SocketEvents.WriteBufferEmpty, Defaults.TxDisconnectTimeout);

		TcpDisconnectLogRecordParams params;
		params.m_address = m_serverRemoteAddress;
		params.m_flags = TcpDisconnectLogRecordFlags.Remote;

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

		writeLog(SocketLogRecordCode.Disconnected, &params, sizeof(params));

		m_serverConnectionSocket.close();
		m_serverConnectionSocket = null;
		m_serverConnectionState = ServerConnectionState.Closed;
		disconnectTarget();
		return;
	}

	if (triggeredEvents & io.SocketEvents.IncomingData)
		forwardDataFromServerToClient();

	waitServerConnectionSocketEvents();
}

TcpProxySession.onClientConnectionSocketEvent(
	uint_t syncId,
	io.SocketEvents triggeredEvents
	)
{
	if (syncId != m_clientConnectionSyncId)
		return;

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

	if (triggeredEvents & io.SocketEvents.Connected)
	{
		writeLog(SocketLogRecordCode.ConnectCompleted);
		m_clientConnectionState = ClientConnectionState.Connected;

		if (m_serverPendingData.m_size)
		{
			writeLog(log.StdRecordCode.Tx, m_serverPendingData.m_p, m_serverPendingData.m_size);
			m_clientConnectionSocket.write(m_serverPendingData.m_p, m_serverPendingData.m_size);
			m_serverPendingData.clear();
		}

		if (m_serverConnectionState != ServerConnectionState.Connected)
		{
			m_clientConnectionSocket.blockingWait(io.SocketEvents.WriteBufferEmpty, Defaults.TxDisconnectTimeout);
			disconnect();
			return;
		}
	}

	if (triggeredEvents & io.SocketEvents.Disconnected)
	{
		for (;;) // read all the buffered data
		{
			size_t result = forwardDataFromClientToServer();
			if (!result)
				break;
		}

		if (m_serverConnectionState == ClientConnectionState.Connected)
			m_serverConnectionSocket.blockingWait(io.SocketEvents.WriteBufferEmpty, Defaults.TxDisconnectTimeout);

		TcpDisconnectLogRecordParams params;
		params.m_address = m_clientRemoteAddress;
		params.m_flags = TcpDisconnectLogRecordFlags.Remote;

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

		writeLog(SocketLogRecordCode.Disconnected, &params, sizeof(params));

		m_clientConnectionSocket.close();
		m_clientConnectionState = ClientConnectionState.Closed;
		disconnect();
		return;
	}

	if (triggeredEvents & io.SocketEvents.IncomingData)
		forwardDataFromClientToServer();

	waitClientConnectionSocketEvents();
}

TcpProxySession.onResolveCompleted(
	uint_t syncId,
	io.SocketAddress const* addressTable,
	size_t addressCount,
	std.Error const* error
	)
{
	if (syncId != m_resolverSyncId)
		return;

	if (error)
	{
		writeLog(SocketLogRecordCode.ResolveError, error, error.m_size);
		m_listenerState = ListenerState.Closed;
		return;
	}

	writeLog(
		SocketLogRecordCode.ResolveCompleted,
		addressTable,
		sizeof(io.SocketAddress)
		);

	m_clientRemoteAddress = addressTable[0];
	if (!m_clientRemoteAddress.m_ip4.m_port) // IPv4 & IPv6 share the same port location
		m_clientRemoteAddress.m_ip4.m_port = m_connectionSocketPropertySet.m_remotePortProp.m_value;

	if (!m_wasTargetAddressChangedWhileResolving)
		m_isTargetAddressResolved = true;

	listenImpl();
}

size_t errorcode TcpProxySession.forwardDataFromClientToServer()
{
	size_t result = m_clientConnectionSocket.read(m_readBuffer.m_p, m_readBuffer.m_size);
	if (!result)
		return 0;

	writeLog(log.StdRecordCode.Rx, m_readBuffer.m_p, result);

	if (m_serverConnectionState == ServerConnectionState.Connected)
		m_serverConnectionSocket.write(m_readBuffer.m_p, result);

	return result;
}

size_t errorcode TcpProxySession.forwardDataFromServerToClient()
{
	size_t result = m_serverConnectionSocket.read(m_readBuffer.m_p, m_readBuffer.m_size);
	if (!result)
		return 0;

	if (m_clientConnectionState != ClientConnectionState.Connected)
	{
		m_serverPendingData.append(m_readBuffer.m_p, result);
	}
	else
	{
		writeLog(log.StdRecordCode.Tx, m_readBuffer.m_p, result);
		m_clientConnectionSocket.write(m_readBuffer.m_p, result);
	}

	return result;
}

TcpProxySession.handleListenError(std.Error const* error)
{
	writeLog(
		SocketLogRecordCode.ListenError,
		error,
		error.m_size
		);

	m_listenerSocket.close();
	m_listenerState = ListenerState.Closed;
	m_listenerSyncId++;
	m_resolverSyncId++;
}

TcpProxySession.handleConnectError(std.Error const* error)
{
	writeLog(
		SocketLogRecordCode.ConnectError,
		error,
		error.m_size
		);

	m_clientConnectionSocket.close();
	m_clientConnectionState = ClientConnectionState.Closed;
}

TcpProxySession.waitListenerSocketEvents()
{
	m_listenerSocket.wait(
		io.SocketEvents.IoError |
		io.SocketEvents.IncomingConnection,
		onListenerSocketEvent ~(++m_listenerSyncId) @ m_pluginHost.m_mainThreadScheduler
		);
}

TcpProxySession.waitServerConnectionSocketEvents()
{
	m_serverConnectionSocket.wait(
		io.SocketEvents.IoError |
		io.SocketEvents.IncomingData |
		io.SocketEvents.Disconnected |
		io.SocketEvents.Reset,
		onServerConnectionSocketEvent ~(++m_serverConnectionSyncId) @ m_pluginHost.m_mainThreadScheduler
		);
}

TcpProxySession.waitClientConnectionSocketEvents()
{
	io.SocketEvents eventMask = io.SocketEvents.IoError;

	if (m_clientConnectionState != ClientConnectionState.Connected)
		eventMask |= io.SocketEvents.Connected;
	else
		eventMask |=
			io.SocketEvents.IncomingData |
			io.SocketEvents.Disconnected |
			io.SocketEvents.Reset;

	m_clientConnectionSocket.wait(
		eventMask,
		onClientConnectionSocketEvent ~(++m_clientConnectionSyncId) @ m_pluginHost.m_mainThreadScheduler
		);
}

reactor TcpProxySession.m_uiReactor
{
	m_title = $"TCP @ $(m_serverPortCombo.m_editText) -> $(m_clientAddressCombo.m_editText)";
	m_actionTable[ActionId.Listen].m_text = m_listenerState ? "Stop Listening" : "Listen";
	m_actionTable[ActionId.Listen].m_icon = m_iconTable[m_listenerState ? IconId.StopListen : IconId.Listen];
	m_actionTable[ActionId.Disconnect].m_isEnabled = m_serverConnectionState || m_clientConnectionState;
	m_statusPaneTable[StatusPaneId.ServerRemoteAddress].m_text = m_serverConnectionState ? m_serverRemoteAddress.getString() : "<server-peer>";
	m_statusPaneTable[StatusPaneId.ServerRemoteAddress].m_isVisible = m_serverConnectionState != ServerConnectionState.Closed;
	m_statusPaneTable[StatusPaneId.ListenerState].m_text = m_listenerStateStringTable[m_listenerState];
	m_statusPaneTable[StatusPaneId.ClientConnectionState].m_text = m_clientConnectionStateStringTable[m_clientConnectionState];
}

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