//..............................................................................
//
// SSL Listener session
//

import "doc_Session.jnc"
import "io_websocket.jncx"
import "io_WebSocket.jnc"
import "iox_WebSocket.jnc"
import "ui_History.jnc"
import "ui_SocketUi.jnc"
import "ui_SslPropertySet.jnc"
import "ui_BufferPropertySet.jnc"
import "ui_StdSessionInfoSet.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"
import "std_List.jnc"
import "std_Buffer.jnc"

using extension ui.ComboBoxHistory;

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

class WebSocketServerSession: doc.Session {
protected:
	enum Defaults {
		LocalPort = 8088, // 8443,
	}

	enum ListenerState {
		Closed,
		Listening,
	}

	enum ConnectionState {
		Closed,
		SslHandshaking,
		WebSocketHandshaking,
		Connected,
		Closing,
	}

	enum ActionId {
		UseSsl,
		Listen,
		Disconnect,
		DisconnectAll,
		ApplyFilter,
		Ping,
		_Count,
	}

	enum IconId {
		Listen,
		StopListen,
		_Count,
	}

	enum StatusPaneId {
		ListenerState,
		ClientCount,
		_Count,
	}

	class Client {
		std.ListEntry* m_entry;
		ConnectionState bindable m_state;
		bool bindable m_isTransmitEnabled;
		io.WebSocket* m_socket;
		io.SocketAddress m_address;
		uint_t m_syncId;
	}

protected:
	// listener socket

	ListenerState bindable m_listenerState;
	io.WebSocket m_listenerSocket;
	io.SocketAddress m_localAddress;
	uint_t m_listenerSyncId;
	std.List m_clientList;
	size_t bindable m_clientCount;
	io.SocketAddress m_currentClientAddress;
	std.Buffer m_readBuffer;
	SocketClientLogFilter m_filter;

	// common websocket ui

	ui.WebSocketUi m_webSocketUi;

	// 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_portCombo;
	ui.ComboBox* m_clientCombo;
	ui.ComboBox* m_clientFilterCombo;

	// property grid

	ui.BoolProperty* m_useSslProp;
	ui.SocketPropertySet m_socketPropertySet;
	ui.SslPropertySet m_sslPropertySet;
	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 disconnect();
	void disconnectAll();
	void ping();

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

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

	override void disconnectSession() {
		disconnectAll();
	}

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

	void onPortComboEnter() {
		try listen();
	}

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

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

	void onClientSocketEvent(
		Client* client,
		uint_t syncId,
		io.WebSocketEvents triggeredEvents
	);

	bool errorcode applyClientFilter(bool rebuildLog);

	void createUi();
	void waitListenerSocketEvents();
	void waitClientSocketEvents(Client* client);
	void disconnect(Client* client);
	void addClient(Client* client);
	void removeClient(Client* client);
	void setCurrentClient(Client* client);
	void configureSsl(io.SslState* sslState);

	void addDisconnectLogRecord(
		Client* client,
		uint_t flags
	);
}

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

WebSocketServerSession.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));
	pluginHost.m_log.addFilter(m_filter);
}

void WebSocketServerSession.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 to filter...";
	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.addLabel("Client:");
	m_clientCombo = m_pluginHost.m_toolBar.addComboBox(-1);

	m_actionTable[ActionId.Disconnect] = createAction("Disconnect", "images/disconnect.png");
	m_actionTable[ActionId.Disconnect].m_onTriggered += disconnect;
	m_actionTable[ActionId.DisconnectAll] = createAction("Disconnect All", "images/disconnect-all.png");
	m_actionTable[ActionId.DisconnectAll].m_onTriggered += disconnectAll;

	m_pluginHost.m_toolBar.addSeparator();
	m_webSocketUi.createCombos(m_pluginHost.m_toolBar);
	m_actionTable[ActionId.Ping] = createAction("Ping", "images/ping.png");
	m_actionTable[ActionId.Ping].m_onTriggered += ping;

	m_pluginHost.m_toolBar.addSeparator();
	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.UseSsl] = createAction("Secure WebWockets (wss://)", "images/ssl.png");
	m_actionTable[ActionId.UseSsl].m_isCheckable = true;
	m_actionTable[ActionId.Listen] = createAction("Listen", m_iconTable[IconId.Listen]);
	m_actionTable[ActionId.Listen].m_onTriggered += onListen;

	// status bar

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

	// property grid

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

	m_webSocketUi.createProperties(m_pluginHost.m_propertyGrid, propGroup);

	propGroup = m_pluginHost.m_propertyGrid.createGroupProperty(
		"TCP",
		"TCP-specific connection settings"
	);

	m_socketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		propGroup,,
		ui.SocketPropertySet.PropertyId.AllTcpServer
	);

	m_socketPropertySet.m_defaultLocalPort = Defaults.LocalPort;

	m_useSslProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		"SSL",
		"Use secure WebSockets (wss://) over SSL"
	);

	m_useSslProp.m_isGroupEnabler = true;

	m_sslPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		m_useSslProp,,
		ui.SslPropertySet.PropertyId.AllServer
	);

	m_bufferPropertySet.createPropertiesGrouped(
		m_pluginHost.m_propertyGrid,,,
		ui.BufferPropertySet.PropertyId.All & ~ui.BufferPropertySet.PropertyId.ReadParallelism
	);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("WebSocket server");

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

	bool isSecure = m_actionTable[ActionId.UseSsl].m_isChecked;
	m_listenerSocket.open(&address, isSecure);
	m_listenerSocket.listen();

	uint64_t recordCode = isSecure ?
		WebSocketLogRecordCode.SecureListening :
		SocketLogRecordCode.Listening;

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

catch:
	m_logWriter.writeLastError(SocketLogRecordCode.ListenError);
	m_listenerSocket.close();
	m_listenerState = ListenerState.Closed;
	m_listenerSyncId++;
	m_stdSessionInfoSet.endSession();
	return false;
}

void WebSocketServerSession.stopListen() {
	if (m_listenerState != ListenerState.Listening)
		return;

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

void WebSocketServerSession.disconnect() {
	Client* client = m_clientCombo.m_currentData;
	if (client)
		disconnect(client);
}

void WebSocketServerSession.ping() {
	Client* client = m_clientCombo.m_currentData;
	if (!client)
		return;

	setCurrentClient(client);
	client.m_socket.writePing();
	m_logWriter.write(WebSocketLogRecordCode.TxPing);
}

void WebSocketServerSession.disconnectAll() {
	while (m_clientList.m_count)
		disconnect(m_clientList.m_head.m_data);
}

void WebSocketServerSession.disconnect(WebSocketServerSession.Client* client) {
	if (m_webSocketUi.m_closeModeProp.m_value == ui.WebSocketCloseMode.Graceful) {
		client.m_socket.writeClose();
		m_logWriter.write(WebSocketLogRecordCode.TxClose);
		client.m_state = ConnectionState.Closing;
		client.m_isTransmitEnabled = false;
		return;
	}

	addDisconnectLogRecord(
		client,
		(client.m_socket.m_options & io.SocketOptions.TcpReset) ?
			TcpDisconnectLogRecordFlags.Reset :
			0
		);

	removeClient(client);
}

void WebSocketServerSession.addDisconnectLogRecord(
	Client* client,
	uint_t flags
) {
	TcpDisconnectLogRecordParams params;
	params.m_address = client.m_address;
	params.m_flags = flags;

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

void WebSocketServerSession.addClient(WebSocketServerSession.Client* client) {
	client.m_entry = m_clientList.add(client);
	m_clientCombo.addItemSetCurrent(client.m_address.getString(), client);
	m_clientCount = m_clientList.m_count;
	m_currentClientAddress = client.m_address;
}

void WebSocketServerSession.removeClient(WebSocketServerSession.Client* client) {
	client.m_socket.close();
	client.m_syncId++;

	m_clientList.remove(client.m_entry);

	for (size_t i = 0; i < m_clientCombo.m_itemCount; i++)
		if (m_clientCombo.m_itemData[i] == client) {
			m_clientCombo.removeItem(i);
			break;
		}

	m_clientCount = m_clientList.m_count;
	m_currentClientAddress = null;
}

void WebSocketServerSession.setCurrentClient(WebSocketServerSession.Client* client) {
	if (m_currentClientAddress.isEqual(client.m_address))
		return;

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

	m_currentClientAddress = client.m_address;
}

bool errorcode WebSocketServerSession.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 WebSocketServerSession.load(doc.Storage* storage) {
	m_portCombo.loadHistory(storage, "portHistory");
	m_adapterCombo.m_currentIndex  = storage.readInt("adapterIdx");
	m_portCombo.m_editText = storage.readString("port");
	m_actionTable[ActionId.UseSsl].m_isChecked = storage.readBool("isSecure");

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

void WebSocketServerSession.save(doc.Storage* storage) {
	m_webSocketUi.save(storage);
	m_socketPropertySet.save(storage);
	m_sslPropertySet.save(storage);
	m_bufferPropertySet.save(storage);

	m_portCombo.saveHistory(storage, "portHistory");
	storage.writeInt("adapterIdx", (int) m_adapterCombo.m_currentIndex);
	storage.writeString("port", m_portCombo.m_editText);
	storage.writeBool("isSecure", m_actionTable[ActionId.UseSsl].m_isChecked);
}

void WebSocketServerSession.updateProperties() {
	m_webSocketUi.updateProperties();
	m_socketPropertySet.m_localPortProp.m_value = atoi(m_portCombo.m_editText);
	m_socketPropertySet.m_adapterProp.m_currentIndex = m_adapterCombo.m_currentIndex;
	m_useSslProp.m_value = m_actionTable[ActionId.UseSsl].m_isChecked;
}

bool errorcode WebSocketServerSession.applyProperties() {
	m_portCombo.m_editText = $"$(m_socketPropertySet.m_localPortProp.m_value)";
	m_adapterCombo.m_currentIndex = m_socketPropertySet.m_adapterProp.m_currentIndex;
	m_actionTable[ActionId.UseSsl].m_isChecked = m_useSslProp.m_value;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
	m_webSocketUi.applyProperties();

	std.ListEntry* entry = m_clientList.m_head;
	for (; entry; entry = entry.m_next) {
		Client* connection = entry.m_data;
		connection.m_socket.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
		connection.m_socket.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
		connection.m_socket.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
		connection.m_socket.m_options =
			ui.getSocketOptions(m_socketPropertySet, m_bufferPropertySet) |
			io.WebSocketOptions.IncludeControlFrames |
			io.WebSocketOptions.DisableCloseResponse |
			io.WebSocketOptions.DisablePongResponse;
	}

	return true;
}

void WebSocketServerSession.restoreDefaultProperties() {
	m_webSocketUi.restoreDefaults();
	m_socketPropertySet.restoreDefaults();
	m_sslPropertySet.restoreDefaults();
	m_bufferPropertySet.restoreDefaults();
	m_useSslProp.m_value = false;
}

size_t errorcode WebSocketServerSession.transmit(
	void const* p,
	size_t size
) {
	Client* client = m_clientCombo.m_currentData;
	if (!client) {
		std.setError("not connected");
		return -1;
	}

	setCurrentClient(client);

	size_t result = client.m_socket.writeText(p, size); // choose text/binary
	if (result)
		m_logWriter.write(log.StdRecordCode.Tx, p, result);

	if (result < size && client.m_isTransmitEnabled) {
		client.m_isTransmitEnabled = false;
		waitClientSocketEvents(client);
	}

	return result;
}

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

void WebSocketServerSession.waitClientSocketEvents(WebSocketServerSession.Client* client) {
	io.WebSocketEvents eventMask =
		io.WebSocketEvents.IoError |
		io.WebSocketEvents.TcpDisconnected |
		io.WebSocketEvents.TcpReset;

	switch (client.m_state) {
	case ConnectionState.SslHandshaking:
		eventMask |= io.WebSocketEvents.SslHandshakeCompleted;
		break;

	case ConnectionState.WebSocketHandshaking:
		eventMask |= io.WebSocketEvents.WebSocketHandshakeCompleted;
		break;

	case ConnectionState.Connected:
		eventMask |= io.WebSocketEvents.IncomingData;

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

		break;

	case ConnectionState.Closing:
		eventMask |=
			io.WebSocketEvents.IncomingData |
			io.WebSocketEvents.WebSocketCloseCompleted;
		break;
	}

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

void WebSocketServerSession.configureSsl(io.SslState* sslState) {
	sslState.enableCiphers(m_sslPropertySet.m_cipherSetProp.m_value);

	if (m_sslPropertySet.m_useEphemeralDhProp.m_value)
		sslState.setEphemeralDhStdParams(m_sslPropertySet.m_ephemeralDhStdParamsProp.m_value);

	if (m_sslPropertySet.m_useEphemeralEcdhProp.m_value)
		sslState.setEphemeralEcdhCurve(m_sslPropertySet.m_ephemeralEcdhCurveProp.m_value);

	if (m_sslPropertySet.m_useCertificateProp.m_value) {
		sslState.loadCertificate(m_sslPropertySet.m_certificateFileProp.m_value);
		sslState.loadPrivateKey(m_sslPropertySet.m_privateKeyFileProp.m_value);

		if (m_sslPropertySet.m_verifyCertificateProp.m_value) {
			sslState.m_verifyMode = io.SslVerifyMode.VerifyPeerCertificate;
			sslState.m_verifyDepth = m_sslPropertySet.m_verifyDepthProp.m_value;

			sslState.loadVerifyLocations(
				m_sslPropertySet.m_verifyCaFileProp.m_value,
				m_sslPropertySet.m_verifyCaDirProp.m_value
			);
		}
	}
}

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

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

	if (triggeredEvents & io.WebSocketEvents.IncomingConnection) {
		io.WebSocket* socket;

	nestedscope:
		io.SocketAddress address;
		socket = m_listenerSocket.accept(&address, true);
		m_logWriter.write(SocketLogRecordCode.ClientConnected, &address, sizeof(address));

		socket.m_extraHeaders = iox.buildWebSocketExtraHeaders(m_webSocketUi.m_extraHeadersProp.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_socketPropertySet, m_bufferPropertySet) |
			io.WebSocketOptions.IncludeControlFrames |
			io.WebSocketOptions.DisableCloseResponse |
			io.WebSocketOptions.DisablePongResponse;

		Client* client = new Client;
		client.m_socket = socket;
		client.m_address = address;

		if (socket.m_sslState) {
			configureSsl(socket.m_sslState);
			client.m_state = ConnectionState.SslHandshaking;
			m_logWriter.write(SslLogRecordCode.SslHandshaking);
		} else {
			client.m_state = ConnectionState.WebSocketHandshaking;
		}

		addClient(client);
		waitClientSocketEvents(client);
		socket.unsuspend();

	catch:
		m_logWriter.writeLastError(SocketLogRecordCode.ConnectError);

		if (socket)
			socket.close();
	}

	waitListenerSocketEvents();
}

void WebSocketServerSession.onClientSocketEvent(
	WebSocketServerSession.Client* client,
	uint_t syncId,
	io.WebSocketEvents triggeredEvents
) {
	if (syncId != client.m_syncId)
		return;

	if (triggeredEvents & io.WebSocketEvents.IoError) {
		setCurrentClient(client);
		m_logWriter.write(log.StdRecordCode.Error, client.m_socket.m_ioError);
		removeClient(client);
		return;
	}

	if (triggeredEvents & io.WebSocketEvents.SslHandshakeCompleted) {
		setCurrentClient(client);
		m_logWriter.write(SslLogRecordCode.SslHandshakeCompleted);

		std.Buffer buffer;
		size_t count = client.m_socket.m_sslState.m_peerCertificateChainLength;
		for (size_t i = 0; i < count; i++) {
			client.m_socket.m_sslState.m_peerCertificateChain[i].encode(buffer, io.SslCertFormat.Der);
			m_logWriter.write(SslLogRecordCode.SslCertificate, buffer.m_p, buffer.m_size);
		}

		io.SslCertificate const* cert = client.m_socket.m_sslState.m_peerCertificate;
		if (cert) {
			cert.encode(buffer, io.SslCertFormat.Der);
			m_logWriter.write(SslLogRecordCode.SslCertificate, buffer.m_p, buffer.m_size);
		}

		m_logWriter.write(SslLogRecordCode.SslCipher, client.m_socket.m_sslState.m_currentCipher.m_name);

		client.m_state = ConnectionState.WebSocketHandshaking;
	}

	if (triggeredEvents & io.WebSocketEvents.WebSocketHandshakeCompleted) {
		setCurrentClient(client);

		m_logWriter.write(
			WebSocketLogRecordCode.WebSocketHandshakeRequest,
			client.m_socket.m_handshakeRequest.m_rawData
		);

		m_logWriter.write(
			WebSocketLogRecordCode.WebSocketHandshakeResponse,
			client.m_socket.m_handshakeResponse.m_rawData
		);

		m_logWriter.write(WebSocketLogRecordCode.WebSocketConnectCompleted);
		client.m_state = ConnectionState.Connected;
		client.m_isTransmitEnabled = true;
		m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	}

	if (triggeredEvents & io.WebSocketEvents.WebSocketCloseCompleted) {
		setCurrentClient(client);

		while ( // read all the buffered data
			iox.processWebSocketIncomingData(
				client.m_socket,
				m_logWriter,
				m_readBuffer.m_p,
				m_readBuffer.m_size
			));

		addDisconnectLogRecord(
			client,
			(client.m_socket.m_options & io.SocketOptions.TcpReset) ?
				TcpDisconnectLogRecordFlags.Reset :
				0
			);

		removeClient(client);
		return;
	}

	if (triggeredEvents & io.WebSocketEvents.TcpDisconnected) {
		setCurrentClient(client);

		while ( // read all the buffered data
			iox.processWebSocketIncomingData(
				client.m_socket,
				m_logWriter,
				m_readBuffer.m_p,
				m_readBuffer.m_size
			));

		addDisconnectLogRecord(
			client,
			(triggeredEvents & io.WebSocketEvents.TcpReset) ?
				TcpDisconnectLogRecordFlags.Remote | TcpDisconnectLogRecordFlags.Reset :
				TcpDisconnectLogRecordFlags.Remote
			);

		removeClient(client);
		return;
	}

	if (triggeredEvents & io.WebSocketEvents.IncomingData) {
		setCurrentClient(client);
		m_clientCombo.m_currentData = client;
		iox.processWebSocketIncomingData(client.m_socket, m_logWriter, m_readBuffer.m_p, m_readBuffer.m_size);
	}

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

	waitClientSocketEvents(client);
}

reactor WebSocketServerSession.m_uiReactor {
	m_title = $"WebSocket @ $(m_portCombo.m_editText)";
	m_useSslProp.m_isEnabled =
	m_actionTable[ActionId.UseSsl].m_isEnabled = !m_listenerState;
	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.Ping].m_isEnabled =
	m_actionTable[ActionId.Disconnect].m_isEnabled =
	m_actionTable[ActionId.DisconnectAll].m_isEnabled = m_clientCombo.m_currentData;
	m_statusPaneTable[StatusPaneId.ListenerState].m_text = m_listenerState ? "Listening" : "Not listening";
	m_statusPaneTable[StatusPaneId.ClientCount].m_text = $"$m_clientCount client(s)";
	m_clientCombo.m_isEnabled = m_clientCount;

	m_isTransmitEnabled =
		m_clientCombo.m_currentData &&
		((Client*)m_clientCombo.m_currentData).m_isTransmitEnabled;

	m_sessionConnectionState = m_clientCombo.m_currentData ?
		SessionConnectionState.Connected :
		SessionConnectionState.Disconnected;
}

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