//..............................................................................
//
// TCP Proxy 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"
import "std_List.jnc"
import "std_HashTable.jnc"

using extension ui.ComboBoxHistory;

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

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

	enum ListenerState {
		Closed,
		Listening,
	}

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

	enum ConnectionState {
		Closed,
		Resolving,
		Connecting,
		Connected,
	}

	enum ActionId {
		Listen,
		DisconnectAll,
		ApplyFilter,
		_Count,
	}

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

	enum StatusPaneId {
		ListenerState,
		TargetAddress,
		ClientCount,
		_Count,
	}

	class Connection {
		ConnectionState m_state;
		io.Socket* m_socket;
		io.SocketAddress m_address;
		std.Buffer m_pendingData;
		bool m_canTransmit;
		uint_t m_syncId;
	};

	class Proxy {
		std.ListEntry* m_proxyListEntry;
		std.ListEntry* m_resolverWaitListEntry;
		Connection m_client;
		Connection m_target;
	}

protected:
	// listener

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

	// proxies

	std.List m_proxyList;
	size_t bindable m_proxyCount;
	Proxy* m_currentProxy;
	iox.HostNameResolver m_resolver;
	std.List m_resolverWaitList;
	std.Buffer m_readBuffer;
	SocketClientLogFilter m_filter;

	// 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_clientFilterCombo;
	ui.ComboBox* m_adapterCombo;
	ui.ComboBox* m_portCombo;
	ui.ComboBox* m_targetAddressCombo;

	// property grid

	ui.SocketPropertySet m_listenerSocketPropertySet;
	ui.SocketPropertySet m_connectionSocketPropertySet;
	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 listen();
	void stopListen();
	void disconnectAll();

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

protected:
	void onListen() {
		m_listenerState ? stopListen() : try listen();
	}

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

	void onTargetAddressEnter() {
		try resolve();
	}

	void onPortComboEnter() {
		try listen();
	}

	void onSetClientFilter() {
		try applyClientFilter(true);
	}

	void onResolveCompleted();

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

	void onConnectionSocketEvent(
		Proxy* proxy,
		Connection* connection,
		uint_t syncId,
		io.SocketEvents triggeredEvents
	);

	bool errorcode applyClientFilter(bool rebuildLog);

	void createUi();

	void handleConnectError(
		Proxy* proxy,
		std.Error const* error
	);

	void handleListenError(std.Error const* error);
	void waitListenerSocketEvents();

	void waitConnectionSocketEvents(
		Proxy* proxy,
		Connection* connection,
		bool peerCanTransmit
	);

	void connect(Proxy* proxy);
	void disconnect(Proxy* proxy);
	bool errorcode resolve();
	void removeProxy(Proxy* proxy);
	void setCurrentProxy(Proxy* proxy);
	void syncConnectCompleted(Proxy* proxy);

	void forwardData(
		TcpProxySession.Proxy* proxy,
		Connection* dst,
		Connection* src
	);
}

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

TcpProxySession.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));
	pluginHost.m_log.addFilter(m_filter);
	m_resolver.m_onCompleted += onResolveCompleted;
}

void TcpProxySession.createUi() {
	// toolbar

	m_pluginHost.m_toolBar.addLabel("Filter:");
	m_clientFilterCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_clientFilterCombo.m_isEditable = true;
	m_clientFilterCombo.m_onEnter += onSetClientFilter;
	m_clientFilterCombo.m_placeholderText = "Enter client address or port...";
	m_clientFilterCombo.m_toolTipText = "View only communications with the specified client";

	m_actionTable[ActionId.ApplyFilter] = createAction("Apply filter", "images/apply.png");
	m_actionTable[ActionId.ApplyFilter].m_onTriggered += onSetClientFilter;
	m_pluginHost.m_toolBar.addSeparator();

	m_pluginHost.m_toolBar.addLabel("Target:");
	m_targetAddressCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_targetAddressCombo.m_isEditable = true;
	bindingof(m_targetAddressCombo.m_editText) += onTargetAddressChanged;
	m_targetAddressCombo.m_onEnter += onTargetAddressEnter;
	m_targetAddressCombo.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("Port:");
	m_portCombo = m_pluginHost.m_toolBar.addComboBox();
	m_portCombo.m_isEditable = true;
	m_portCombo.m_onEnter += onPortComboEnter;

	m_iconTable[IconId.Listen] = new ui.Icon("images/listen.png");
	m_iconTable[IconId.StopListen] = new ui.Icon("images/pause.png");
	m_actionTable[ActionId.Listen] = createAction("Listen", m_iconTable [IconId.Listen]);
	m_actionTable[ActionId.Listen].m_onTriggered += onListen;
	m_actionTable[ActionId.DisconnectAll] = createAction("Disconnect All", "images/disconnect-all.png");
	m_actionTable[ActionId.DisconnectAll].m_onTriggered += disconnectAll;

	// status bar

	m_statusPaneTable[StatusPaneId.TargetAddress] = m_pluginHost.m_statusBar.addPane(, 80);
	m_statusPaneTable[StatusPaneId.ListenerState] = m_pluginHost.m_statusBar.addPane(, 80);
	m_statusPaneTable[StatusPaneId.ClientCount] = m_pluginHost.m_statusBar.addPane(, 80);

	// property grid

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

	m_listenerSocketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		group,,
		ui.SocketPropertySet.PropertyId.AllTcpServer &
		~ui.SocketPropertySet.PropertyId.TcpOptions
	);

	m_listenerSocketPropertySet.m_defaultLocalPort = Defaults.LocalPort;

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

	m_connectionSocketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		group,,
		ui.SocketPropertySet.PropertyId.RemoteAddress |
		ui.SocketPropertySet.PropertyId.RemotePort |
		ui.SocketPropertySet.PropertyId.TcpOptions
	);

	m_bufferPropertySet.createPropertiesGrouped(m_pluginHost.m_propertyGrid);

	// information grid

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

	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 TcpProxySession.listen() {
	stopListen();
	startSession();

	m_portCombo.addEditTextToHistory();

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

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

	address = m_listenerSocket.m_address;
	m_logWriter.write(SocketLogRecordCode.Listening, &address, sizeof(address));
	m_localAddress = address;
	m_listenerState = ListenerState.Listening;
	waitListenerSocketEvents();
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	return true;

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

bool errorcode TcpProxySession.resolve() {
	m_resolver.cancel();
	m_targetAddressCombo.addEditTextToHistory();
	char const* addressString = m_targetAddressCombo.m_editText;
	m_resolver.m_defaultPort = m_connectionSocketPropertySet.m_remotePortProp.m_value;
	return m_resolver.resolve(addressString);
}

void TcpProxySession.stopListen() {
	if (!m_listenerState)
		return;

	m_logWriter.write(SocketLogRecordCode.ListenStopped, &m_localAddress, sizeof(m_localAddress));
	m_listenerSocket.close();
	m_listenerState = ListenerState.Closed;
	m_listenerSyncId++;
	m_stdSessionInfoSet.endSession();
}

void TcpProxySession.disconnectAll() {
	while (m_proxyList.m_count)
		disconnect(m_proxyList.m_head.m_data);
}

void TcpProxySession.disconnect(TcpProxySession.Proxy* proxy) {
	TcpDisconnectLogRecordParams params;
	params.m_flags = (m_connectionSocketPropertySet.m_tcpResetProp.m_value) ?
		TcpDisconnectLogRecordFlags.Reset :
		0;

	switch (proxy.m_target.m_state) {
	case ConnectionState.Connecting:
		syncConnectCompleted(proxy);
		m_logWriter.write(SocketLogRecordCode.ConnectCancelled);
		break;

	case ConnectionState.Connected:
		proxy.m_target.m_socket.blockingWait(
			io.SocketEvents.WriteBufferEmpty,
			Defaults.TxDisconnectTimeout
		);

		params.m_address = proxy.m_target.m_address;
		m_logWriter.write(SocketLogRecordCode.Disconnected, &params, sizeof(params));
		break;
	}

	if (proxy.m_client.m_state != ConnectionState.Connected) { // already disconnected
		m_logWriter.write(SocketLogRecordCode.ClientRemoved);
	} else {
		proxy.m_client.m_socket.blockingWait(
			io.SocketEvents.WriteBufferEmpty,
			Defaults.TxDisconnectTimeout
		);

		params.m_address = proxy.m_client.m_address;
		m_logWriter.write(SocketLogRecordCode.ClientDisconnected, &params, sizeof(params));
	}

	removeProxy(proxy);
}

void TcpProxySession.removeProxy(TcpProxySession.Proxy* proxy) {
	proxy.m_client.m_socket.close();
	proxy.m_client.m_state = ConnectionState.Closed;
	proxy.m_client.m_syncId++;
	proxy.m_target.m_socket.close();
	proxy.m_target.m_state = ConnectionState.Closed;
	proxy.m_target.m_syncId++;

	if (proxy.m_resolverWaitListEntry)
		m_resolverWaitList.remove(proxy.m_resolverWaitListEntry);

	m_proxyList.remove(proxy.m_proxyListEntry);
	m_proxyCount = m_proxyList.m_count;
	m_currentProxy = null;
}

void TcpProxySession.setCurrentProxy(TcpProxySession.Proxy* proxy) {
	if (proxy == m_currentProxy)
		return;

	m_logWriter.write(
		SocketLogRecordCode.ClientChanged,
		&proxy.m_client.m_address,
		sizeof(proxy.m_client.m_address)
	);

	m_currentProxy = proxy;
}

void TcpProxySession.syncConnectCompleted(TcpProxySession.Proxy* proxy) {
	if (proxy == m_currentProxy)
		return;

	setCurrentProxy(proxy);

	m_logWriter.write(
		SocketLogRecordCode.ConnectCompletedPrefix,
		proxy.m_target.m_address,
		sizeof(proxy.m_target.m_address)
	);
}

bool errorcode TcpProxySession.applyClientFilter(bool rebuildLog) {
	m_pluginHost.m_log.suspend();
	bool result = try m_filter.setFilter(m_clientFilterCombo.m_editText);
	m_pluginHost.m_log.resume();

	if (!result) {
		m_clientFilterCombo.m_backgroundColor = ui.StdColor.PastelRed;
		return false;
	}

	m_clientFilterCombo.addEditTextToHistory();
	m_clientFilterCombo.m_backgroundColor = ui.ColorFlags.Transparent;

	if (rebuildLog)
		m_pluginHost.m_log.rebuild();

	return true;
}

void TcpProxySession.connect(TcpProxySession.Proxy* proxy) {
	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:
		proxy.m_target.m_state = ConnectionState.Resolving;
		proxy.m_resolverWaitListEntry = m_resolverWaitList.add(proxy);
		return;

	default:
		assert(false);
	}

	m_currentProxy = proxy;

	m_logWriter.write(SocketLogRecordCode.Connecting, &m_resolver.m_address, sizeof(m_resolver.m_address));
	proxy.m_target.m_socket.open(m_resolver.m_address.m_family, io.Protocol.Tcp);
	proxy.m_target.m_socket.connect(&m_resolver.m_address);
	proxy.m_target.m_address = m_resolver.m_address;
	proxy.m_target.m_state = ConnectionState.Connecting;
	waitConnectionSocketEvents(proxy, proxy.m_target, proxy.m_client.m_canTransmit);

catch:
	handleConnectError(proxy, std.getLastError());
	removeProxy(proxy);
}

void TcpProxySession.load(doc.Storage* storage) {
	m_portCombo.loadHistory(storage, "portHistory");
	m_targetAddressCombo.loadHistory(storage, "targetAddressHistory");
	m_adapterCombo.m_currentIndex  = storage.readInt("adapterIdx");
	m_portCombo.m_editText = storage.readString("port");
	m_targetAddressCombo.m_editText = storage.readString("targetAddress");

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

void TcpProxySession.save(doc.Storage* storage) {
	m_portCombo.saveHistory(storage, "portHistory");
	m_targetAddressCombo.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_portCombo.m_editText);
	storage.writeString("targetAddress", m_targetAddressCombo.m_editText);
}

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

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

void TcpProxySession.restoreDefaultProperties() {
	m_listenerSocketPropertySet.restoreDefaults();
	m_connectionSocketPropertySet.restoreDefaults();
	m_bufferPropertySet.restoreDefaults();
}

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

	if (triggeredEvents & io.SocketEvents.IoError) {
		handleListenError(m_listenerSocket.m_ioError);
		return;
	}

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

		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 = ui.getSocketOptions(m_connectionSocketPropertySet, m_bufferPropertySet);
		socket.unsuspend();

		Proxy* proxy = new Proxy;
		proxy.m_client.m_state = ConnectionState.Connected;
		proxy.m_client.m_socket = socket;
		proxy.m_client.m_address = address;
		proxy.m_client.m_canTransmit = true;
		proxy.m_target.m_socket = new io.Socket;
		proxy.m_target.m_canTransmit = false;
		proxy.m_proxyListEntry = m_proxyList.add(proxy);
		m_proxyCount = m_proxyList.m_count;
		m_currentProxy = proxy;

		waitConnectionSocketEvents(proxy, proxy.m_client, false);
		connect(proxy);
	}

	waitListenerSocketEvents();
}

void TcpProxySession.onConnectionSocketEvent(
	TcpProxySession.Proxy* proxy,
	TcpProxySession.Connection* connection,
	uint_t syncId,
	io.SocketEvents triggeredEvents
) {
	if (syncId != connection.m_syncId)
		return;

	if (triggeredEvents & io.SocketEvents.IoError) {
		if (connection.m_state == ConnectionState.Connecting) {
			syncConnectCompleted(proxy);
			handleConnectError(proxy, connection.m_socket.m_ioError);
		} else {
			setCurrentProxy(proxy);
			m_logWriter.write(log.StdRecordCode.Error, connection.m_socket.m_ioError);
		}

		disconnect(proxy);
		return;
	}

	bool isClient = connection == proxy.m_client;
	Connection* peer = isClient ? &proxy.m_target : &proxy.m_client;

	if (triggeredEvents & io.SocketEvents.TcpConnected) {
		syncConnectCompleted(proxy);
		m_logWriter.write(SocketLogRecordCode.ConnectCompleted);
		connection.m_state = ConnectionState.Connected;
		connection.m_canTransmit = true;
		forwardData(proxy, connection, peer);
		waitConnectionSocketEvents(proxy, peer, true);
	}

	if (triggeredEvents & io.SocketEvents.TcpDisconnected) {
		for (;;) { // read all the buffered data
			size_t size = connection.m_socket.read(m_readBuffer.m_p, m_readBuffer.m_size);
			if (!size)
				break;

			peer.m_pendingData.append(m_readBuffer.m_p, size);
		}

		forwardData(proxy, peer, connection);

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

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

		if (isClient) {
			params.m_flags |= TcpDisconnectLogRecordFlags.KeepClient;
			m_logWriter.write(SocketLogRecordCode.ClientDisconnected, &params, sizeof(params));
		} else {
			setCurrentProxy(proxy);
			m_logWriter.write(SocketLogRecordCode.Disconnected, &params, sizeof(params));
		}

		connection.m_state = ConnectionState.Closed;

		if (peer.m_pendingData.m_size) // otherwise, wait for peer to accept pending data
			m_logWriter.write(
				SocketLogRecordCode.DisconnectDataPending,
				&peer.m_pendingData.m_size,
				sizeof(uint32_t)
			);
		else
			disconnect(proxy);

		return;
	}

	if (triggeredEvents & io.SocketEvents.IncomingData)
		forwardData(proxy, peer, connection);

	if (triggeredEvents & io.SocketEvents.WriteBufferReady) {
		connection.m_canTransmit = true;
		forwardData(proxy, connection, peer);
		waitConnectionSocketEvents(proxy, peer, true);
	}

	if (peer.m_state == ConnectionState.Closed && !connection.m_pendingData.m_size)
		disconnect(proxy);
	else
		waitConnectionSocketEvents(proxy, connection, peer.m_canTransmit);
}

void TcpProxySession.onResolveCompleted() {
	while (m_resolverWaitList.m_count) {
		Proxy* proxy = m_resolverWaitList.removeHead();
		proxy.m_resolverWaitListEntry = null;
		m_resolver.m_state == iox.HostNameResolver.State.Resolved ?
			connect(proxy) :
			disconnect(proxy);
	}
}

void TcpProxySession.forwardData(
	TcpProxySession.Proxy* proxy,
	TcpProxySession.Connection* dst,
	TcpProxySession.Connection* src
) {
	void const* p;
	size_t size;

	if (dst.m_pendingData.m_size) {
		p = dst.m_pendingData.m_p;
		size = dst.m_pendingData.m_size;
	} else {
		p = m_readBuffer.m_p;
		size = src.m_socket.read(m_readBuffer.m_p, m_readBuffer.m_size);
	}

	if (!size)
		return;

	size_t forwardSize = dst.m_canTransmit ? dst.m_socket.write(p, size) : 0;
	if (forwardSize) {
		setCurrentProxy(proxy);
		uint64_t recordCode = src == proxy.m_client ? log.StdRecordCode.Tx : log.StdRecordCode.Rx;
		m_logWriter.write(recordCode, p, forwardSize);
	}

	if (forwardSize == size) {
		dst.m_pendingData.clear();
		return;
	}

	if (p == dst.m_pendingData.m_p)
		dst.m_pendingData.remove(0, forwardSize);
	else
		dst.m_pendingData.copy(p + forwardSize, size - forwardSize);

	dst.m_canTransmit = false;
	waitConnectionSocketEvents(proxy, dst, src.m_canTransmit);
}

void TcpProxySession.handleListenError(std.Error const* error) {
	uint64_t recordCode = m_listenerState == ListenerState.Closed ?
		SocketLogRecordCode.ListenError :
		log.StdRecordCode.Error;

	m_logWriter.write(recordCode, error);
	m_listenerSocket.close();
	m_listenerState = ListenerState.Closed;
	m_listenerSyncId++;
	m_stdSessionInfoSet.endSession();
}

void TcpProxySession.handleConnectError(
	TcpProxySession.Proxy* proxy,
	std.Error const* error
) {
	m_logWriter.write(SocketLogRecordCode.ConnectError, error);
	proxy.m_target.m_socket.close();
	proxy.m_target.m_state = ConnectionState.Closed;
}

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

void TcpProxySession.waitConnectionSocketEvents(
	TcpProxySession.Proxy* proxy,
	TcpProxySession.Connection* connection,
	bool peerCanTransmit
) {
	io.SocketEvents eventMask = io.SocketEvents.IoError;

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

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

		if (peerCanTransmit)
			eventMask |= io.SocketEvents.IncomingData;

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

		break;

	default:
		return;
	}

	connection.m_socket.wait(
		eventMask,
		onConnectionSocketEvent~(proxy, connection, ++connection.m_syncId) @ m_pluginHost.m_mainThreadScheduler
	);
}

reactor TcpProxySession.m_uiReactor {
	m_title = $"TCP @ $(m_portCombo.m_editText) \u2192 $(m_targetAddressCombo.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.DisconnectAll].m_isEnabled = m_proxyCount;
	m_statusPaneTable[StatusPaneId.ListenerState].m_text = m_listenerStateStringTable[m_listenerState];
	m_statusPaneTable[StatusPaneId.ClientCount].m_text = $"$m_proxyCount client(s)";

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

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