//..............................................................................
//
// TCP Connection session
//

import "io_base.jncx"
import "io_Socket.jnc"
import "io_SocketAddressResolver.jnc"
import "std_Buffer.jnc"
import "doc_Plugin.jnc"
import "gui_History.jnc"

using extension gui.ComboBoxHistory;

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

class TcpConnectionSession: doc.Session
{
protected:
	enum State
	{
		Closed,
		Resolving,
		Connecting,
		Connected,
	}

	static char const* m_stateStringTable [] =
	{
		"Closed",
		"Resolving...",
		"Connecting...",
		"Connected",
	}

	enum ActionId
	{
		Connect,
		_Count,
	}

	enum IconId
	{
		Connect,
		Disconnect,
		_Count,
	}

	enum StatusPaneId
	{
		RemoteAddress,
		State,
		_Count,
	}

protected:

	// socket

	State bindable m_state;
	io.Socket m_socket;
	io.SocketAddressResolver m_resolver;
	io.SocketAddress m_remoteAddress;
	std.Buffer m_readBuffer;
	uint_t m_syncId;

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

	// property grid

	io.SocketPropertySet m_socketPropertySet;
	io.BufferPropertySet m_bufferPropertySet;

public:
	construct (doc.PluginHost* pluginHost);

	bool errorcode connect ();
	bool errorcode connect (io.SocketAddress const* address);
	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
		);

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

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

	onAddressComboEnter ()
	{
		try connect ();
	}

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

	onSocketEvent (
		uint_t syncId,
		io.SocketEvents triggeredEvents
		);

	createUi ();
	waitSocketEvents ();

	reactor m_uiReactor ();
}

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

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

	pluginHost.m_log.attachRepresenter (representTcpConnectionLog);

	m_state = State.Closed;
	createUi ();
}

TcpConnectionSession.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_iconTable [IconId.Connect] = new gui.Icon ("images/connect.png");
	m_iconTable [IconId.Disconnect] = new gui.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 = gui.StdColor.Blue;
	m_statusPaneTable [StatusPaneId.State] = m_pluginHost.m_statusBar.addPane (, 80);

	// property grid

	gui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty (
		"Connection",
		"TCP-specific connection settings"
		);

	m_socketPropertySet.createProperties (m_pluginHost.m_propertyGrid, group);

	group = m_pluginHost.m_propertyGrid.createGroupProperty (
		"Buffering",
		"Advanced buffering options affecting throughtput and compatibility"
		);

	m_bufferPropertySet.createProperties (m_pluginHost.m_propertyGrid, group);

	m_uiReactor.start ();
}

bool errorcode TcpConnectionSession.connect ()
{
	disconnect ();

	startSession ();

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

	io.SocketAddress address;
	bool isDirectAddress = try address.parse (addressString);
	if (isDirectAddress)
	{
		if (!address.m_ip4.m_port) // IPv4 & IPv6 share the same port location
			address.m_ip4.m_port = m_socketPropertySet.m_remotePortProp.m_value;

		return try connect (&address);
	}

	m_state = State.Resolving;

	writeLog (
		TcpConnectionLogRecordCode.Resolving,
		addressString,
		strlen (addressString)
		);

	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 (addressString, addressFamily, onResolveCompleted ~(++m_syncId) @ m_pluginHost.m_mainThreadScheduler);
	return true;

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

bool errorcode TcpConnectionSession.connect (io.SocketAddress const* remoteAddress)
{
	disconnect ();

	io.SocketAddress localAddress;

nestedscope:
	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 = io.getSocketOptions (m_socketPropertySet, m_bufferPropertySet);

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

		localAddress.m_ip4.m_port = (uint16_t) m_socketPropertySet.m_localPortProp.m_value; // same for IPv4/IPv6

		m_socket.open (io.Protocol.Tcp, &localAddress);
	}

	m_state = State.Connecting;
	m_remoteAddress = *remoteAddress;

	m_socket.connect (remoteAddress);

	localAddress = m_socket.m_address;
	char const* p = $"$(*remoteAddress; B)$(localAddress; B)";
	writeLog (TcpConnectionLogRecordCode.ConnectingFrom, p, dynamic sizeof (p));

	waitSocketEvents ();
	return true;

catch:
	if (!m_socketPropertySet.m_useLocalAddressProp.m_value)
	{
		writeLog (TcpConnectionLogRecordCode.Connecting, remoteAddress, sizeof (io.SocketAddress));
	}
	else
	{
		char const* p = $"$(*remoteAddress; B)$(localAddress; B)";
		writeLog (TcpConnectionLogRecordCode.ConnectingFrom, p, dynamic sizeof (p));
	}

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

TcpConnectionSession.waitSocketEvents ()
{
	io.SocketEvents eventMask = io.SocketEvents.IoError;

	switch (m_state)
	{
	case State.Connecting:
		eventMask |= io.SocketEvents.Connected;
		break;

	case State.Connected:
		eventMask |=
			io.SocketEvents.IncomingData |
			io.SocketEvents.Disconnected |
			io.SocketEvents.Reset;

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

		break;
	}

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

TcpConnectionSession.handleSocketError (std.Error const* error)
{
	uint_t recordCode = m_state < State.Connected ?
		TcpConnectionLogRecordCode.ConnectError :
		log.StdRecordCode.Error;

	writeLog (recordCode, error, error.m_size);
	m_socket.close ();
	m_state = State.Closed;
}

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

	case State.Resolving:
	case State.Connecting:
		writeLog (TcpConnectionLogRecordCode.ConnectCancelled);
		break;

	case State.Connected:
		TcpDisconnectLogRecordParams params;
		params.m_address = m_remoteAddress;
		params.m_flags = 0;

		if (m_socketPropertySet.m_tcpResetProp.m_value)
			params.m_flags |= TcpDisconnectLogRecordFlags.Reset;

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

		m_isTransmitEnabled = false;
		break;
	}

	m_socket.close ();
	m_state = State.Closed;
	m_syncId++;
}

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

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

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

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

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

bool errorcode TcpConnectionSession.applyProperties ()
{
	m_addressCombo.m_editText = m_socketPropertySet.m_remoteAddressProp.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 = io.getSocketOptions (m_socketPropertySet, m_bufferPropertySet);
	return true;
}

void TcpConnectionSession.restoreDefaultProperties ()
{
	m_socketPropertySet.restoreDefaults ();
	m_bufferPropertySet.restoreDefaults ();
}

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

	size_t result = m_socket.write (p, size);
	if (result)
		writeLog (log.StdRecordCode.Tx, p, result);

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

	return result;
}

TcpConnectionSession.onSocketEvent (
	uint_t syncId,
	io.SocketEvents triggeredEvents
	)
{
	if (syncId != m_syncId)
		return;

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

	if (triggeredEvents & io.SocketEvents.Connected)
	{
		writeLog (TcpConnectionLogRecordCode.ConnectCompleted);
		m_state = State.Connected;
		m_isTransmitEnabled = true;
	}

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

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

		TcpDisconnectLogRecordParams recordParams;
		recordParams.m_address = m_remoteAddress;
		recordParams.m_flags = TcpDisconnectLogRecordFlags.Remote;

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

		writeLog (
			TcpConnectionLogRecordCode.Disconnected,
			&recordParams,
			sizeof (recordParams)
			);

		m_socket.close ();
		m_state = State.Closed;
		m_isTransmitEnabled = false;
		return;
	}

	if ((triggeredEvents & io.SocketEvents.ReadBufferFull) &&
		m_bufferPropertySet.m_notifyReadBufferFullProp.m_value)
		writeLog (log.StdRecordCode.RxBufferFull);

	if (triggeredEvents & io.SocketEvents.IncomingData)
	{
		size_t result = m_socket.read (m_readBuffer.m_p, m_readBuffer.m_size);
		if (result)
			writeLog (log.StdRecordCode.Rx, m_readBuffer.m_p, result);
	}

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

	waitSocketEvents ();
}

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

	if (error)
	{
		handleSocketError (error);
		return;
	}

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

	m_state = State.Closed;

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

	connect (address);
}

reactor TcpConnectionSession.m_uiReactor ()
{
	m_title = $"TCP $(m_addressCombo.m_editText)";
	m_socketPropertySet.m_adapterProp.m_isEnabled = m_socketPropertySet.m_useLocalAddressProp.m_value;
	m_socketPropertySet.m_localPortProp.m_isEnabled = m_socketPropertySet.m_useLocalAddressProp.m_value;
	m_socketPropertySet.m_reuseLocalAddressProp.m_isEnabled = m_socketPropertySet.m_useLocalAddressProp.m_value;
	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_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;
}

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