//.............................................................................
//
// UDP session
//

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

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

class UdpSession: doc.Session
{
protected:
	enum State
	{
		Closed,
		Opened,
	}

	enum ResolveState
	{
		Unresolved,
		Resolving,
		Resolved,
		ChangeWhileResolving, // address changed while currently resolving
	}
	
	enum ActionId
	{
		AutoRemoteAddress,
		Open,
		_Count,
	}

	enum IconId
	{
		Open,
		Close,
		Compass,
		_Count,
	}

	enum StatusPaneId
	{
		State,
		_Count,
	}
	
protected:
	// socket
		
	State bindable m_state;
	UdpLogFilter m_logFilter;
	io.Socket m_socket;
	io.SocketAddressResolver m_resolver;
	ResolveState m_resolveState;
	io.SocketAddress m_remoteAddress;
	char* m_pendingData;
	size_t m_pendingDataSize;
	bool m_remoteAddressChanged;
	
	// 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_localPortCombo;
	gui.ComboBox* m_remoteAddressCombo;
	
	// property grid
	
	gui.StringProperty* m_remoteAddressProp;
	gui.BoolProperty* m_autoRemoteAddressProp;
	gui.EnumProperty* m_adapterProp;
	gui.IntProperty* m_localPortProp;
	gui.BoolProperty* m_reuseAddressProp;
	gui.BoolProperty* m_packetHdrVisibleProp;
	
		
public:
	construct (doc.PluginHost* pluginHost);

	openSocket ();
	closeSocket ();

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

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

	override size_t errorcode transmitTo (
		void const* p,
		size_t size,
		void const* address,
		size_t addressSize
		);

protected:
	void reportError();

	onOpen  ();

	onLocalPortChanged ();
	onRemoteAddressChanged ();
	onSocketEvent (io.SocketEventParams const* params);
	onResolverEvent (io.SocketAddressResolverEventParams const* params);
	
	createGui ();
	reactor m_uiReactor ();
}

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

UdpSession.construct (doc.PluginHost* pluginHost)
{
	basetype.construct(pluginHost);
	
	pluginHost.m_log.m_representerFunc = representUdpLog;
	pluginHost.m_log.attachFilter (m_logFilter);

	m_state = State.Closed;
	m_socket.m_onSocketEvent += onSocketEvent @ pluginHost.m_mainThreadScheduler;
	m_resolver.m_onResolverEvent += onResolverEvent @ pluginHost.m_mainThreadScheduler;
	m_resolveState = ResolveState.Unresolved;
	m_remoteAddressChanged = false;

	createGui ();
}

UdpSession.createGui()
{
	// toolbar

	m_iconTable [IconId.Open] = new gui.Icon ("images/open.png");
	m_iconTable [IconId.Close] = new gui.Icon ("images/close.png");
	m_iconTable [IconId.Compass] = new gui.Icon ("images/compass.png");

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

	m_pluginHost.m_toolBar.addLabel ("Port:");
	m_localPortCombo = m_pluginHost.m_toolBar.addComboBox ();
	m_localPortCombo.m_isEditable = true;
	m_localPortCombo.m_onChanged += onLocalPortChanged;

	m_actionTable [ActionId.Open] = createAction("Open socket", m_iconTable [IconId.Open]);
	m_actionTable [ActionId.Open].m_onTriggered += onOpen;

	m_pluginHost.m_toolBar.addLabel ("Remote:");
	m_remoteAddressCombo = m_pluginHost.m_toolBar.addComboBox (-1);
	m_remoteAddressCombo.m_isEditable = true;
	m_remoteAddressCombo.m_onChanged += onRemoteAddressChanged;

	m_actionTable [ActionId.AutoRemoteAddress] = createAction("Auto-switch remote address", m_iconTable [IconId.Compass]);
	m_actionTable [ActionId.AutoRemoteAddress].m_isCheckable = true;

	// status bar

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

	// property grid

	m_remoteAddressProp = m_pluginHost.m_propertyGrid.createStringProperty (
		"Remote address",
		"Remote address to send to (IP-address or host-name followed by ':' port)"
		);

	m_autoRemoteAddressProp = m_pluginHost.m_propertyGrid.createBoolProperty (
		"Auto-switch remote address",
		"Automatically switch remote address upon receiving a datagram"
		);

	m_adapterProp = io.createNetworkAdapterProperty (
		m_pluginHost.m_propertyGrid,
		io.NetworkAdapterUsage.Listener,
		"Adapter",
		"Adapter to bind socket to"
		);
		
	m_localPortProp = m_pluginHost.m_propertyGrid.createIntProperty (
		"Local UDP port",
		"Local UDP port to listen to"
		);

	m_reuseAddressProp = m_pluginHost.m_propertyGrid.createBoolProperty (
		"Re-use UDP port",
		"Allow multiple UDP sockets to share the same port"
		);

	m_packetHdrVisibleProp = m_pluginHost.m_propertyGrid.createBoolProperty (
		"Show packet headers",
		"Display packet header info records (or filter them out)"
		);

	m_uiReactor.start();
}

UdpSession.onOpen  ()
{
	if (m_state)
		closeSocket ();
	else
		openSocket ();
}

UdpSession.openSocket ()
{
	closeSocket ();
	startSession ();
	
	char const* portString = m_localPortCombo.m_editText;
	m_localPortCombo.addEditTextToHistory ();

	io.SocketAddress address = *(io.SocketAddress const*) m_adapterCombo.m_currentData;
	address.m_ip4.m_port = (uint16_t) atoi (portString); // same for IPv4/IPv6

	io.SocketOpenFlags flags = io.SocketOpenFlags.Asynchronous;
	if (m_reuseAddressProp.m_value)
		flags |= io.SocketOpenFlags.ReuseAddress;
	
	if (!try m_socket.open (io.Protocol.Udp, &address, flags))
	{
		reportError ();
		return;
	}
	
	address = m_socket.m_address;
	writeLog (UdpRecordCode.Open, &address, sizeof (address));

	m_state = State.Opened;
}

UdpSession.closeSocket ()
{
	if (m_state == State.Closed)
		return;

	io.SocketAddress address = m_socket.m_address;
	m_socket.close ();

	m_localPortCombo.m_backgroundColor = gui.StdColor.White;

	writeLog (UdpRecordCode.Close, &address, sizeof (address));
	
	m_state = State.Closed;
}

UdpSession.load (doc.Storage* storage)
{	
	m_adapterCombo.m_currentIndex  = storage.readInt ("adapterIdx");
	m_localPortCombo.m_editText = storage.readString ("localPort");
	m_reuseAddressProp.m_value = storage.readBool ("reuseAddress");
	m_remoteAddressCombo.m_editText = storage.readString ("remoteAddress");	
	m_actionTable [ActionId.AutoRemoteAddress].m_isChecked = storage.readInt ("autoRemoteAddress");	
	m_logFilter.m_isPacketHdrVisible = storage.readBool ("isPacketHdrVisible", true);
}

UdpSession.save (doc.Storage* storage)
{
	storage.writeInt ("adapterIdx", (int) m_adapterCombo.m_currentIndex);
	storage.writeString ("localPort", m_localPortCombo.m_editText);
	storage.writeBool ("reuseAddress", m_reuseAddressProp.m_value);
	storage.writeString ("remoteAddress", m_remoteAddressCombo.m_editText);
	storage.writeInt ("autoRemoteAddress", m_actionTable [ActionId.AutoRemoteAddress].m_isChecked);
	storage.writeBool ("isPacketHdrVisible", m_logFilter.m_isPacketHdrVisible);
}

UdpSession.updateProperties ()
{
	m_autoRemoteAddressProp.m_value = m_actionTable [ActionId.AutoRemoteAddress].m_isChecked;
	m_remoteAddressProp.m_value = m_remoteAddressCombo.m_editText;
	m_localPortProp.m_value = atoi (m_localPortCombo.m_editText);
	m_packetHdrVisibleProp.m_value = m_logFilter.m_isPacketHdrVisible;
}

bool errorcode UdpSession.applyProperties ()
{
	m_actionTable [ActionId.AutoRemoteAddress].m_isChecked = m_autoRemoteAddressProp.m_value;
	m_remoteAddressCombo.m_editText = m_remoteAddressProp.m_value;
	m_localPortCombo.m_editText = $"$(m_localPortProp.m_value)";
	m_logFilter.m_isPacketHdrVisible = m_packetHdrVisibleProp.m_value;
	return true;
}

UdpSession.restoreDefaultProperties ()
{
	m_adapterProp.m_currentIndex = 0;
	m_localPortProp.m_value = 0;
	m_remoteAddressProp.m_value = "127.0.0.1:1001";
	m_autoRemoteAddressProp.m_value = false;
	m_packetHdrVisibleProp.m_value = true;
}

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

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

	io.SocketAddress remoteAddress;
	bool isDirectAddress = try remoteAddress.parse (addressString);
	if (isDirectAddress || m_resolveState == ResolveState.Resolved)
	{	
		if (isDirectAddress)
			m_remoteAddress = remoteAddress;
		else
			remoteAddress = m_remoteAddress; // resolved address
		
		m_socket.sendTo (p, size, &remoteAddress);
		
		m_pluginHost.m_log.m_writer.write (UdpRecordCode.TxPacket, &remoteAddress, sizeof (io.SocketAddress));
		m_pluginHost.m_log.m_writer.write (log.StdRecordCode.Tx, p, size);
	}
	else
	{
		m_pendingDataSize = size;
		m_pendingData = new char [m_pendingDataSize];
		memcpy (m_pendingData, p, m_pendingDataSize);
		
		if (m_resolveState == ResolveState.Unresolved)
		{
			writeLog (
				UdpRecordCode.Resolving,
				addressString,
				strlen (addressString)
				);

			io.AddressFamily addressFamily = io.AddressFamily.Undefined;
			io.SocketAddress const* adapterAddress = m_adapterCombo.m_currentData;
			if (adapterAddress)
				addressFamily = adapterAddress.m_family;

			m_resolver.m_syncId = m_socket.m_syncId;
			m_resolver.resolve (addressString, addressFamily);
			m_resolveState = ResolveState.Resolving;
		}
	}
	
	return size;

catch:
	reportError ();
	return -1;
}

size_t errorcode UdpSession.transmitTo (
	void const* p,
	size_t size,
	void const* address,
	size_t addressSize
	)
{
	if (m_state == State.Closed || addressSize < sizeof (io.SocketAddress))
		return -1;

	io.SocketAddress const* remoteAddress = (io.SocketAddress const*) address;		
	m_socket.sendTo (p, size, remoteAddress);
		
	m_pluginHost.m_log.m_writer.write (UdpRecordCode.TxPacket, remoteAddress, sizeof (io.SocketAddress));
	m_pluginHost.m_log.m_writer.write (log.StdRecordCode.Tx, p, size);	
	return size;
}

UdpSession.reportError()
{
	std.Error const* error = std.getLastError ();
	writeLog (UdpRecordCode.Error, error, error.m_size);
}

UdpSession.onLocalPortChanged ()
{
	if (m_state == State.Opened)
		m_localPortCombo.m_backgroundColor = gui.StdColor.PastelOrange;
}

UdpSession.onRemoteAddressChanged ()
{
	m_resolveState = m_resolveState == ResolveState.Resolving ?
		ResolveState.ChangeWhileResolving :
		ResolveState.Unresolved;
}

UdpSession.onSocketEvent(io.SocketEventParams const* params)
{
	if (params.m_syncId != m_socket.m_syncId)
		return;

	switch (params.m_eventKind)
	{
	case io.SocketEvent.IncomingData:
		io.SocketAddress remoteAddress;
		static char buffer [1600]; // static is OK -- we read in main thread
		size_t size = try m_socket.recvFrom (buffer, sizeof (buffer), &remoteAddress);
		if (size == -1)
			break;
		
		if (m_actionTable [ActionId.AutoRemoteAddress].m_isChecked)
			m_remoteAddressCombo.m_editText = remoteAddress.getString ();
			
		writeLog (UdpRecordCode.RxPacket, &remoteAddress, sizeof (io.SocketAddress));
		writeLog (log.StdRecordCode.Rx, buffer, size);
		break;
	}
}

UdpSession.onResolverEvent (io.SocketAddressResolverEventParams const* params)
{
	if (params.m_syncId != m_socket.m_syncId)
		return;
		
	switch (params.m_eventKind)
	{
	case io.SocketAddressResolverEvent.ResolveCompleted:
		writeLog (
			UdpRecordCode.ResolveCompleted,
			params.m_addressTable,
			sizeof (io.SocketAddress)
			);

		m_remoteAddress = *params.m_addressTable;
		
		m_resolveState = m_resolveState == ResolveState.ChangeWhileResolving ?
			ResolveState.Unresolved :
			ResolveState.Resolved;
		
		size_t result = try m_socket.sendTo (m_pendingData, m_pendingDataSize, &m_remoteAddress);
		if (result == -1)
		{
			reportError ();
			break;
		}
		
		m_pluginHost.m_log.m_writer.write(UdpRecordCode.TxPacket, &m_remoteAddress, sizeof (io.SocketAddress));
		m_pluginHost.m_log.m_writer.write(log.StdRecordCode.Tx, m_pendingData, m_pendingDataSize);
		m_pendingData = null;
		m_pendingDataSize = 0;
		break;

	case io.SocketAddressResolverEvent.ResolveError:
		writeLog (UdpRecordCode.Error, params.m_error, params.m_error.m_size);
		m_resolveState = ResolveState.Unresolved;
		break;
		
	default:
		m_resolveState = ResolveState.Unresolved;
		break;
	}
}

reactor UdpSession.m_uiReactor ()
{
	m_title = $"UDP @ $(m_localPortCombo.m_editText) -> $(m_remoteAddressCombo.m_editText)";
	m_isTransmitEnabled = m_state == State.Opened;
	m_remoteAddressCombo.m_isEnabled = !m_actionTable [ActionId.AutoRemoteAddress].m_isChecked;
	m_actionTable [ActionId.Open].m_text = m_state ? "Close Socket" : "Open Socket";
	m_actionTable [ActionId.Open].m_icon = m_iconTable [m_state ? IconId.Close : IconId.Open];
	m_statusPaneTable [StatusPaneId.State].m_text = m_state ? "Opened" : "Closed";
}

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