//..............................................................................
//
// Pipe Server session
//

import "doc_Session.jnc"
import "io_base.jncx"
import "io_NamedPipe.jnc"
import "io_FileStream.jnc"
import "ui_History.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"

using extension ui.ComboBoxHistory;

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

class PipeServerSession: doc.Session {
protected:
	enum Defaults {
		MessageMode        = false,
		ConnectParallelism = 2,
	}

	enum State {
		Closed,
		Listening,
	}

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

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

	enum StatusPaneId {
		ListenerState,
		ClientCount,
		_Count,
	}

	class Client {
		std.ListEntry* m_entry;
		io.FileStream* m_pipe;
		uint_t m_syncId;
		bool bindable m_isTransmitEnabled;
	}

protected:
	// listener

	State bindable m_state;
	io.NamedPipe m_listenerPipe;
	uint_t m_listenerSyncId;
	char const* m_pipeName;
	std.List m_clientList;
	size_t bindable m_clientCount;
	Client* m_currentClient;
	std.Buffer m_readBuffer;
	PipeClientLogFilter 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_pipeNameCombo;
	ui.ComboBox* m_clientCombo;
	ui.ComboBox* m_clientFilterCombo;

	// property grid

	ui.StringProperty* m_pipeNameProp;
	ui.BoolProperty* m_messageModeProp;
	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();

	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_state ? stopListen() : try listen();
	}

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

	void onPipeNameComboEnter() {
		try listen();
	}

	void onListenerPipeEvent(
		uint_t syncId,
		io.NamedPipeEvents triggeredEvents
	);

	void onClientPipeEvent(
		Client* client,
		uint_t syncId,
		io.FileStreamEvents triggeredEvents
	);

	void createUi();
	bool errorcode applyClientFilter(bool rebuildLog);
	io.FileStreamOptions getFileStreamOptions();
	void waitListenerPipeEvents();
	void waitClientPipeEvents(Client* client);
	void disconnect(Client* client);
	void removeClient(Client* client);
	void setCurrentClient(Client* client);
}

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

PipeServerSession.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 PipeServerSession.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 ID 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.addSeparator();

	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_pluginHost.m_toolBar.addLabel("Name:");
	m_pipeNameCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_pipeNameCombo.m_isEditable = true;
	m_pipeNameCombo.m_onEnter += onPipeNameComboEnter;
	m_pipeNameCombo.m_placeholderText = "Enter a pipe name...";

	m_iconTable[IconId.Listen] = new ui.Icon("images/listen.png");
	m_iconTable[IconId.StopListen] = new ui.Icon("images/pause.png");
	m_iconTable[IconId.Disconnect] = new ui.Icon("images/disconnect.png");

	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* group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Connection",
		"Named pipe-specific connection settings"
	);

	m_pipeNameProp = m_pluginHost.m_propertyGrid.createStringProperty(
		group,,
		"Pipe name",
		"Enter a pipe name...",
		"Specify pipe name to listen on"
	);

	m_messageModeProp =  m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Message mode",
		"Read and write using the named pipe message mode"
	);

	m_bufferPropertySet.createPropertiesGrouped(m_pluginHost.m_propertyGrid);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("Pipe listener");

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

	m_pipeNameCombo.addEditTextToHistory();

	char const* name = m_pipeNameCombo.m_editText;

	io.FileStreamOptions options = getFileStreamOptions();

	if (m_messageModeProp.m_value)
		options |= io.FileStreamOptions.MessageNamedPipe;

	m_listenerPipe.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_listenerPipe.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_listenerPipe.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_listenerPipe.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
	m_listenerPipe.m_options = options;
	m_listenerPipe.open(name);

	m_logWriter.write(
		PipeServerLogRecordCode.Listening,
		name,
		strlen(name)
	);

	m_pipeName = name;
	m_state = State.Listening;

	waitListenerPipeEvents();
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	return true;

catch:
	m_logWriter.writeLastError(PipeServerLogRecordCode.ListenError);
	m_listenerPipe.close();
	return false;
}

void PipeServerSession.stopListen() {
	if (m_state != State.Listening)
		return;

	m_logWriter.write(
		PipeServerLogRecordCode.ListenStopped,
		m_pipeName,
		strlen(m_pipeName)
	);

	m_listenerPipe.close();
	m_state = State.Closed;
	m_listenerSyncId++;
	m_stdSessionInfoSet.endSession();
}

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

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

void PipeServerSession.disconnect(PipeServerSession.Client* client) {
	PipeDisconnectLogRecordParams params;
	params.m_id = client.m_pipe.m_osHandle;
	m_logWriter.write(PipeServerLogRecordCode.ClientDisconnected, &params, sizeof(params));
	removeClient(client);
}

void PipeServerSession.removeClient(PipeServerSession.Client* client) {
	client.m_pipe.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_currentClient = null;
}

void PipeServerSession.setCurrentClient(PipeServerSession.Client* client) {
	if (client == m_currentClient)
		return;

	uint64_t id = client.m_pipe.m_osHandle;
	m_logWriter.write(PipeServerLogRecordCode.ClientChanged, &id, sizeof(id));
	m_currentClient = client;
}

bool errorcode PipeServerSession.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;
}

io.FileStreamOptions PipeServerSession.getFileStreamOptions() {
	io.FileStreamOptions options;

	if (m_bufferPropertySet.m_keepReadBlockSizeProp.m_value)
		options |= io.FileStreamOptions.KeepReadBlockSize;

	if (m_bufferPropertySet.m_keepWriteBlockSizeProp.m_value)
		options |= io.FileStreamOptions.KeepWriteBlockSize;

	return options;
}

void PipeServerSession.waitListenerPipeEvents() {
	io.NamedPipeEvents eventMask =
		io.NamedPipeEvents.IoError |
		io.NamedPipeEvents.IncomingConnection;

	m_listenerPipe.wait(eventMask, onListenerPipeEvent~(++m_listenerSyncId) @ m_pluginHost.m_mainThreadScheduler);
}

void PipeServerSession.waitClientPipeEvents(PipeServerSession.Client* client) {
	io.FileStreamEvents eventMask =
		io.FileStreamEvents.IoError |
		io.FileStreamEvents.ReadBufferFull |
		io.FileStreamEvents.IncomingData |
		io.FileStreamEvents.Eof;

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

	client.m_pipe.wait(
		eventMask,
		onClientPipeEvent~(client, ++client.m_syncId) @ m_pluginHost.m_mainThreadScheduler
	);
}

void PipeServerSession.load(doc.Storage* storage) {
	m_pipeNameCombo.loadHistory(storage, "pipeNameHistory");
	m_pipeNameCombo.m_editText = storage.readString("pipeName");
	m_messageModeProp.m_value = storage.readBool("messageMode", Defaults.MessageMode);

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

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

	m_pipeNameCombo.saveHistory(storage, "pipeNameHistory");
	storage.writeString("pipeName", m_pipeNameCombo.m_editText);
	storage.writeBool("messageMode", m_messageModeProp.m_value);
}

void PipeServerSession.updateProperties() {
	m_pipeNameProp.m_value = m_pipeNameCombo.m_editText;
}

bool errorcode PipeServerSession.applyProperties() {
	m_pipeNameCombo.m_editText = m_pipeNameProp.m_value;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);

	io.FileStreamOptions options = getFileStreamOptions();

	if (m_listenerPipe.m_isOpen) {
		m_listenerPipe.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
		m_listenerPipe.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
		m_listenerPipe.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
		m_listenerPipe.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
		m_listenerPipe.m_options = options;
	}

	std.ListEntry* entry = m_clientList.m_head;
	for (; entry; entry = entry.m_next) {
		Client* client = entry.m_data;
		client.m_pipe.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
		client.m_pipe.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
		client.m_pipe.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
		client.m_pipe.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
		client.m_pipe.m_options = options;
	}

	return true;
}

void PipeServerSession.restoreDefaultProperties() {
	m_bufferPropertySet.restoreDefaults();
}

size_t errorcode PipeServerSession.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_pipe.write(p, size);
	if (result)
		m_logWriter.write(log.StdRecordCode.Tx, p, result);

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

	return result;
}

void PipeServerSession.onListenerPipeEvent(
	uint_t syncId,
	io.NamedPipeEvents triggeredEvents
) {
	if (syncId != m_listenerSyncId)
		return;

	if (triggeredEvents & io.NamedPipeEvents.IoError) {
		m_logWriter.write(log.StdRecordCode.Error, m_listenerPipe.m_ioError);
		stopListen();
		return;
	}

	if (triggeredEvents & io.NamedPipeEvents.IncomingConnection) {
		io.FileStream* pipe = m_listenerPipe.accept(true);
		uint64_t id = pipe.m_osHandle;
		m_logWriter.write(PipeServerLogRecordCode.ClientConnected, &id, sizeof(id));

		pipe.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
		pipe.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
		pipe.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
		pipe.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
		pipe.m_options = getFileStreamOptions();
		pipe.unsuspend();

		Client* client = new Client;
		client.m_pipe = pipe;
		client.m_entry = m_clientList.add(client);
		client.m_isTransmitEnabled = true;
		m_clientCombo.addItemSetCurrent($"Client ID 0x%llX"(id), client);
		m_clientCount = m_clientList.m_count;
		m_currentClient = client;

		waitClientPipeEvents(client);
	}

	waitListenerPipeEvents();
}

void PipeServerSession.onClientPipeEvent(
	PipeServerSession.Client* client,
	uint_t syncId,
	io.FileStreamEvents triggeredEvents
) {
	if (syncId != client.m_syncId)
		return;

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

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

	if (triggeredEvents & io.FileStreamEvents.IncomingData) {
		setCurrentClient(client);

		size_t result = client.m_pipe.read(m_readBuffer.m_p, m_readBuffer.m_size);
		if (result)
			m_logWriter.write(log.StdRecordCode.Rx, m_readBuffer.m_p, result);
	}

	if (triggeredEvents & io.FileStreamEvents.Eof) {
		for (;;) { // read all the buffered data
			size_t result = client.m_pipe.read(m_readBuffer.m_p, m_readBuffer.m_size);
			if (!result)
				break;

			setCurrentClient(client);
			m_logWriter.write(log.StdRecordCode.Rx, m_readBuffer.m_p, result);
		}

		PipeDisconnectLogRecordParams params;
		params.m_id = client.m_pipe.m_osHandle;
		params.m_flags = PipeDisconnectLogRecordFlags.Remote;
		m_logWriter.write(PipeServerLogRecordCode.ClientDisconnected, &params, sizeof(params));
		removeClient(client);
		return;
	}

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

	waitClientPipeEvents(client);
}

reactor PipeServerSession.m_uiReactor {
	m_title = $"Pipe $(m_pipeNameCombo.m_editText)";
	m_actionTable[ActionId.Listen].m_text = m_state ? "Stop Listening" : "Listen";
	m_actionTable[ActionId.Listen].m_icon = m_iconTable[m_state ? IconId.StopListen : IconId.Listen];
	m_actionTable[ActionId.Disconnect].m_isEnabled =
	m_actionTable[ActionId.DisconnectAll].m_isEnabled = m_clientCombo.m_currentData;
	m_statusPaneTable[StatusPaneId.ListenerState].m_text = m_state  ? "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;
}

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