//..............................................................................
//
// SSL Connection session
//

import "doc_Session.jnc"
import "io_base.jncx"
import "io_websocket.jncx"
import "io_WebSocket.jnc"
import "io_HostNameResolver.jnc"
import "iox_WebSocket.jnc"
import "ui_History.jnc"
import "ui_SocketUi.jnc"
import "ui_SslPropertySet.jnc"
import "ui_WebSocketUi.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 WebSocketClientSession: doc.Session
{
protected:
	enum State
	{
		Closed,
		Resolving,
		ReconnectDelay,
		TcpConnecting,
		SslHandshaking,
		WebSocketHandshakeRequest,
		WebSocketHandshakeResponse,
		Connected,
		Closing,
	}

	static char const* m_stateStringTable[] =
	{
		"Closed",
		"Resolving...",
		"Waiting...",
		"Connecting...",
		"SSL-handshaking...",
		"WS-handshaking...", // WebSocketHandshakeRequest
		"WS-handshaking...", // WebSocketHandshakeResponse
		"Connected",
		"Closing...",
	}

	enum ActionId
	{
		Connect,
		Ping,
		_Count,
	}

	enum IconId
	{
		Connect,
		Disconnect,
		_Count,
	}

	enum StatusPaneId
	{
		RemoteAddress,
		State,
		_Count,
	}

	enum InfoId
	{
		LocalAddress,
		RemoteAddress,
		_Count,
	}

	enum Defaults
	{
		InsecurePort = 80,
		SecurePort   = 443,
	}

protected:
	// WebSocket

	State bindable m_state;
	io.WebSocket m_socket;
	io.HostNameResolver m_resolver;
	io.SocketAddress m_localAddress;
	io.SocketAddress m_remoteAddress;
	std.Buffer m_readBuffer;
	sys.Timer m_reconnectTimer;
	uint64_t m_lastReconnectTime;
	uint_t m_syncId;

	// 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_addressCombo;

	// property grid

	ui.SocketPropertySet m_socketPropertySet;
	ui.SslPropertySet m_sslPropertySet;
	ui.BufferPropertySet m_bufferPropertySet;

	// information grid

	ui.InformationValue* m_infoTable[InfoId._Count];
	ui.StdSessionInfoSet m_stdSessionInfoSet;
	ui.ThroughputInfoSet m_throughputInfoSet;
	ui.ChecksumInfoSet m_checksumInfoSet;

	// ui reactor

	reactor m_uiReactor;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode connect();

	bool errorcode connect(
		io.SocketAddress const* address,
		char const* resource,
		char const* host,
		bool isSecure = false
		);

	void disconnect();

	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 bool errorcode connectSession()
	{
		return m_sessionConnectionState ? true : connect();
	}

	override void disconnectSession()
	{
		disconnect();
	}

	void ping();

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

	void onConnect()
	{
		m_state ? disconnect() : try connect();
	}

	void onAddressComboEnter()
	{
		try connect();
	}

	void onResolverEvent(
		uint_t syncId,
		char const* resource,
		char const* host,
		bool isSecure,
		io.HostNameResolverEvents triggeredEvents
		);

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

	void onReconnectTimer(uint_t syncId);

	void createUi();
	void startReconnectTimer(bool canConnectNow);
	void waitSocketEvents();
	void addDisconnectLogRecord(uint_t flags);
}

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

WebSocketClientSession.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));
}

void WebSocketClientSession.createUi()
{
	// toolbar

	m_pluginHost.m_toolBar.addLabel("Address:");
	m_addressCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_addressCombo.m_isEditable = true;
	m_addressCombo.m_onEnter += onAddressComboEnter;
	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);

	// property grid

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

	m_pluginHost.m_toolBar.addSeparator();

	m_webSocketUi.createCombos(m_pluginHost.m_toolBar);
	m_webSocketUi.createProperties(m_pluginHost.m_propertyGrid, propGroup);

	m_actionTable[ActionId.Ping] = createAction("Ping", "images/ping.png");
	m_actionTable[ActionId.Ping].m_onTriggered += ping;

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

	m_socketPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		propGroup,,
		(ui.SocketPropertySet.PropertyId.AllTcpClient & ~ui.SocketPropertySet.PropertyId.RemotePort) |
		ui.SocketPropertySet.PropertyId.Reconnect
		);

	m_socketPropertySet.m_defaultRemoteAddress = "ws://demos.kaazing.com/echo";

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

	m_sslPropertySet.createProperties(
		m_pluginHost.m_propertyGrid,
		propGroup,,
		ui.SslPropertySet.PropertyId.AllClient
		);

	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("SSL Connection");
	m_infoTable[InfoId.LocalAddress] = m_pluginHost.m_informationGrid.createValue(infoGroup,,, "Local address");
	m_infoTable[InfoId.RemoteAddress] = m_pluginHost.m_informationGrid.createValue(infoGroup,,, "Remote address");

	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 WebSocketClientSession.connect()
{
	disconnect();

	startSession();

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

	// parse address string

	bool isSecure;
	char const* host;
	char const* resource;

	jnc.RegexState regexState;
	reswitch (regexState, addressString)
	{
	case r"(ws|wss)://([^/?]+)(.*)":
		isSecure = regexState.m_subMatchArray[0].m_length == 3;
		host = regexState.m_subMatchArray[1].m_text;

		jnc.RegexMatch const* resourceSubMatch = &regexState.m_subMatchArray[2];
		resource = resourceSubMatch.m_length && resourceSubMatch.m_text[0] == '/' ?
			resourceSubMatch.m_text :
			strcat("/", resourceSubMatch.m_text);
		break;

	default:
		std.setError("invalid WebSocket URI");
		m_logWriter.writeLastError(log.StdRecordCode.Error);
		return false;
	}

	io.SocketAddress address;
	bool isDirectAddress = try address.parse(host);
	if (isDirectAddress)
	{
		if (!address.m_port)
			address.m_port = isSecure ? Defaults.SecurePort : Defaults.InsecurePort;

		return try connect(&address, resource, host, isSecure);
	}

	m_state = State.Resolving;
	m_sessionConnectionState = SessionConnectionState.Connecting;
	m_logWriter.write(SocketLogRecordCode.Resolving, host);

	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.resolve(host, addressFamily);

	m_resolver.wait(
		io.HostNameResolverEvents.Resolved | io.HostNameResolverEvents.Error,
		onResolverEvent~(++m_syncId, resource, host, isSecure) @ m_pluginHost.m_mainThreadScheduler
		);

	return true;

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

bool errorcode WebSocketClientSession.connect(
	io.SocketAddress const* remoteAddress,
	char const* resource,
	char const* host,
	bool isSecure
	)
{
	disconnect();

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

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

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

		m_socket.open(&m_localAddress, isSecure);
	}

	m_state = State.TcpConnecting;
	m_sessionConnectionState = SessionConnectionState.Connecting;
	m_remoteAddress = *remoteAddress;

	if (isSecure)
	{
		m_socket.m_sslState.enableCiphers(m_sslPropertySet.m_cipherSetProp.m_value);

		if (m_sslPropertySet.m_useCertificateProp.m_value)
		{
			m_socket.m_sslState.loadCertificate(m_sslPropertySet.m_certificateFileProp.m_value);
			m_socket.m_sslState.loadPrivateKey(m_sslPropertySet.m_privateKeyFileProp.m_value);
		}

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

			m_socket.m_sslState.loadVerifyLocations(
				m_sslPropertySet.m_verifyCaFileProp.m_value,
				m_sslPropertySet.m_verifyCaDirProp.m_value
				);
		}
	}

	m_socket.connect(remoteAddress, resource, host);

	m_localAddress = m_socket.m_address;
	char const* p = $"$(*remoteAddress; B)$(m_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)$(m_localAddress; B)";
		m_logWriter.write(SocketLogRecordCode.ConnectingFrom, p, dynamic sizeof(p));
	}

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

void WebSocketClientSession.waitSocketEvents()
{
	io.WebSocketEvents eventMask =
		io.WebSocketEvents.IoError |
		io.WebSocketEvents.TcpDisconnected |
		io.WebSocketEvents.TcpReset;

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

	case State.SslHandshaking:
		eventMask |= io.WebSocketEvents.SslHandshakeCompleted;
		break;

	case State.WebSocketHandshakeRequest:
		eventMask |= io.WebSocketEvents.WebSocketHandshakeRequested;
		break;

	case State.WebSocketHandshakeResponse:
		eventMask |= io.WebSocketEvents.WebSocketHandshakeCompleted;
		break;

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

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

		break;

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

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

void WebSocketClientSession.addDisconnectLogRecord(uint_t flags)
{
	TcpDisconnectLogRecordParams params;
	params.m_address = m_remoteAddress;
	params.m_flags = flags;

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

void WebSocketClientSession.handleSocketError(std.Error const* error)
{
	uint64_t recordCode = m_state < State.Connected ?
		SocketLogRecordCode.ConnectError :
		log.StdRecordCode.Error;

	m_logWriter.write(recordCode, error);
	close();
}

void WebSocketClientSession.close()
{
	m_stdSessionInfoSet.endSession();
	m_isTransmitEnabled = false;
	m_socket.close();
	m_state = State.Closed;
	m_sessionConnectionState = SessionConnectionState.Disconnected;
	m_syncId++;
}

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

	case State.Resolving:
	case State.TcpConnecting:
	case State.SslHandshaking:
	case State.WebSocketHandshakeRequest:
	case State.WebSocketHandshakeResponse:
		m_logWriter.write(SocketLogRecordCode.ConnectCancelled);
		break;

	case State.Connected:
		if (m_webSocketUi.m_closeModeProp.m_value == ui.WebSocketCloseMode.Graceful)
		{
			m_socket.writeClose();
			m_logWriter.write(WebSocketLogRecordCode.TxClose);
			m_state = State.Closing;
			m_isTransmitEnabled = false;
			return;
		}

		// ...and fall-through

	case State.Closing:
		addDisconnectLogRecord(
			(m_socket.m_options & io.SocketOptions.TcpReset) ?
			TcpDisconnectLogRecordFlags.Reset :
			0
			);
		break;
	}

	close();
}

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

	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 WebSocketClientSession.save(doc.Storage* storage)
{
	m_webSocketUi.save(storage);
	m_socketPropertySet.save(storage);
	m_sslPropertySet.save(storage);
	m_bufferPropertySet.save(storage);

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

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

bool errorcode WebSocketClientSession.applyProperties()
{
	m_addressCombo.m_editText = m_socketPropertySet.m_remoteAddressProp.m_value;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
	m_webSocketUi.applyProperties();

	if (!m_socket.m_isOpen)
		return true;

	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) |
		io.WebSocketOptions.IncludeControlFrames |
		io.WebSocketOptions.DisableCloseResponse |
		io.WebSocketOptions.DisablePongResponse;

	return true;
}

void WebSocketClientSession.restoreDefaultProperties()
{
	m_webSocketUi.restoreDefaults();
	m_socketPropertySet.restoreDefaults();
	m_sslPropertySet.restoreDefaults();
	m_bufferPropertySet.restoreDefaults();
}

size_t errorcode WebSocketClientSession.transmit(
	void const* p,
	size_t size
	)
{
	if (m_state != State.Connected)
		return -1;

	size_t result = iox.transmitWebSocketData(
		m_socket,
		m_webSocketUi.m_transmitModeCombo.m_currentData,
		m_logWriter,
		p,
		size
		);

	if (result < size && m_isTransmitEnabled)
	{
		m_isTransmitEnabled = false;
		waitSocketEvents();
	}

	return result;
}

void WebSocketClientSession.ping()
{
	if (m_state != State.Connected)
		return;

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

void WebSocketClientSession.startReconnectTimer(bool isForced)
{
	uint64_t delay = m_socketPropertySet.m_reconnectTimeoutProp.m_value;
	uint64_t dueTime = m_lastReconnectTime + delay * 10000;

	if (!isForced && sys.getTimestamp() >= dueTime) // immediate
	{
		onReconnectTimer(m_syncId);
		return;
	}

	m_state = State.ReconnectDelay;
	m_logWriter.write(SocketLogRecordCode.ReconnectDelay);

	m_reconnectTimer.startDueTimer(
		onReconnectTimer~(++m_syncId) @ m_pluginHost.m_mainThreadScheduler,
		dueTime
		);
}

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

	if (triggeredEvents & io.WebSocketEvents.IoError)
	{
		handleSocketError(m_socket.m_ioError);
		return;
	}

	if (triggeredEvents & io.WebSocketEvents.TcpConnected)
	{
		m_logWriter.write(SocketLogRecordCode.ConnectCompleted);

		if (m_socket.m_sslState)
		{
			m_logWriter.write(SslLogRecordCode.SslHandshaking);
			m_state = State.SslHandshaking;
		}
		else
		{
			m_state = State.WebSocketHandshakeRequest;
		}
	}

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

		// write certificates

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

		m_logWriter.write(SslLogRecordCode.SslCipher, m_socket.m_sslState.m_currentCipher.m_name);
		m_state = State.WebSocketHandshakeRequest;
	}

	if (triggeredEvents & io.WebSocketEvents.WebSocketHandshakeRequested)
	{
		m_logWriter.write(
			WebSocketLogRecordCode.WebSocketHandshakeRequest,
			m_socket.m_handshakeRequest.m_rawData
			);

		m_state = State.WebSocketHandshakeResponse;
	}

	if (triggeredEvents & io.WebSocketEvents.WebSocketHandshakeCompleted)
	{
		m_logWriter.write(
			WebSocketLogRecordCode.WebSocketHandshakeResponse,
			m_socket.m_handshakeResponse.m_rawData
			);

		m_logWriter.write(WebSocketLogRecordCode.WebSocketConnectCompleted);

		m_infoTable[InfoId.LocalAddress].m_value = m_localAddress.getString();
		m_infoTable[InfoId.RemoteAddress].m_value = m_remoteAddress.getString();

		m_state = State.Connected;
		m_sessionConnectionState = SessionConnectionState.Connected;
		m_isTransmitEnabled = true;
		m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	}

	if (triggeredEvents & io.WebSocketEvents.WebSocketCloseCompleted)
	{
		while ( // read all the buffered data
			iox.processWebSocketIncomingData(
				m_socket,
				m_logWriter,
				m_readBuffer.m_p,
				m_readBuffer.m_size
				));

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

		close();
		return;
	}

	if (triggeredEvents & io.WebSocketEvents.TcpDisconnected)
	{
		while ( // read all the buffered data
			iox.processWebSocketIncomingData(
				m_socket,
				m_logWriter,
				m_readBuffer.m_p,
				m_readBuffer.m_size
				));

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

		close();

		if (m_socketPropertySet.m_useReconnectProp.m_value)
			startReconnectTimer(true);

		return;
	}

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

	if (triggeredEvents & io.WebSocketEvents.IncomingData)
		iox.processWebSocketIncomingData(m_socket, m_logWriter, m_readBuffer.m_p, m_readBuffer.m_size);

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

	waitSocketEvents();
}

void WebSocketClientSession.onResolverEvent(
	uint_t syncId,
	char const* resource,
	char const* host,
	bool isSecure,
	io.HostNameResolverEvents triggeredEvents
	)
{
	if (syncId != m_syncId)
		return;

	if (triggeredEvents & io.HostNameResolverEvents.Error)
	{
		handleSocketError(m_resolver.m_error);
		return;
	}

	m_logWriter.write(
		SocketLogRecordCode.ResolveCompleted,
		m_resolver.m_addressTable,
		sizeof(io.SocketAddress)
		);

	m_state = State.Closed;

	io.SocketAddress address = m_resolver.m_addressTable[0];
	if (!address.m_port)
		address.m_port = isSecure ? Defaults.SecurePort : Defaults.InsecurePort;

	try connect(address, resource, host, isSecure);
}

void WebSocketClientSession.onReconnectTimer(uint_t syncId)
{
	if (syncId != m_syncId)
		return;

	m_lastReconnectTime = sys.getTimestamp();

	bool result = try connect();
	if (!result && m_socketPropertySet.m_useReconnectProp.m_value)
		startReconnectTimer(false);
}

reactor WebSocketClientSession.m_uiReactor
{
	m_title = $"WebSocket $(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_actionTable[ActionId.Ping].m_isEnabled = m_state == State.Connected;
	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;
	m_infoTable[InfoId.LocalAddress].m_isEnabled = m_state == State.Connected;
	m_infoTable[InfoId.RemoteAddress].m_isEnabled = m_state == State.Connected;

	onevent bindingof(m_addressCombo.m_editText)()
	{
		m_reconnectTimer.stop();
	}
}

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