//..............................................................................
//
// HID session
//

import "doc_Session.jnc"
import "io_hid.jncx"
import "io_HidRd.jnc"
import "io_HidDevice.jnc"
import "io_HidEnumerator.jnc"
import "ui_History.jnc"
import "ui_BufferPropertySet.jnc"
import "ui_StdSessionInfoSet.jnc"
import "ui_HidUi.jnc"
import "ui_GroupBox.jnc"
import "ui_Button.jnc"
import "ui_DictionaryWidget.jnc"
import "ui_LineEdit.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"
import "std_Buffer.jnc"
import "std_Array.jnc"

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

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

	enum ReadMode {
		Normal,
		Disabled,
	}

	enum ActionId {
		Refresh,
		Open,
		Info,
		_Count,
	}

	enum IconId {
		Refresh,
		Open,
		Close,
		Info,
		Send,
		_Count,
	}

	enum StatusPaneId {
		State,
		_Count,
	}

protected:
	// device

	State bindable m_state;
	io.HidDevice m_device;
	io.HidRd m_rd;
	HidLogReportWriter m_reportWriter;
	std.Buffer m_deviceInfoBuffer;
	std.Buffer m_rdBuffer;
	std.Buffer m_readBuffer;
	uint_t m_syncId;

	// 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_deviceCombo;
	HidReportUi m_reportUi;

	// property grid

	ui.EnumProperty* m_deviceProp;
	ui.EnumProperty* m_readModeProp;
	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 open();
	void close();

	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 : open();
	}

	override void disconnectSession() {
		close();
	}

protected:
	void onRefresh() {
		ui.refreshHidDeviceListUi(m_deviceCombo, m_deviceProp);
	}

	void onOpen() {
		m_state ? close() : try open();
	}

	void onDeviceComboEnter() {
		try open();
	}

	void onInfo() {
		m_logWriter.write(HidLogRecordCode.DeviceInfo, m_deviceInfoBuffer.m_p, m_deviceInfoBuffer.m_size);
		m_logWriter.write(HidLogRecordCode.ReportDescriptor, m_rdBuffer.m_p, m_rdBuffer.m_size);
	}

	void onHidDeviceEvent(
		uint_t syncId,
		io.HidDeviceEvents triggeredEvents
	);

	void createUi();
	void waitHidDeviceEvents();
}

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

HidSession.construct(doc.PluginHost* pluginHost) {
	basetype.construct(pluginHost);
	m_reportWriter.construct(pluginHost.m_log.m_writer);
	m_reportUi.construct(pluginHost, m_reportWriter, m_device, m_rd);

	createUi();

	pluginHost.m_log.addSelectionProcessor(log.ThroughputCalcRangeProcessor.factory~(m_throughputInfoSet));
	pluginHost.m_log.addSelectionProcessor(log.ChecksumCalcRangeProcessor.factory~(m_checksumInfoSet));
}

void HidSession.createUi() {
	// toolbar

	m_pluginHost.m_toolBar.addLabel("Device:");
	m_deviceCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_deviceCombo.m_onReturnPressed += onDeviceComboEnter;

	m_iconTable[IconId.Refresh] = new ui.Icon("images/refresh.png");
	m_iconTable[IconId.Open] = new ui.Icon("images/open.png");
	m_iconTable[IconId.Close] = new ui.Icon("images/close.png");
	m_iconTable[IconId.Info] = new ui.Icon("images/info.png");
	m_iconTable[IconId.Send] = new ui.Icon("images/send.png");

	m_actionTable[ActionId.Refresh] = createAction("Re-enumerate ports", m_iconTable[IconId.Refresh]);
	m_actionTable[ActionId.Refresh].m_onTriggered += onRefresh;
	m_actionTable[ActionId.Open] = createAction("Open", m_iconTable[IconId.Open]);
	m_actionTable[ActionId.Open].m_onTriggered += onOpen;
	m_actionTable[ActionId.Info] = createAction("HID device info", m_iconTable[IconId.Info]);
	m_actionTable[ActionId.Info].m_onTriggered += onInfo;

	// status bar

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

	// property grid

	ui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Connection",
		"File stream-specific connection settings"
	);

	m_deviceProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Device name",
		"Enter a HID device name...",
		null, 0
	);

	ui.EnumPropertyOption readModeOptionTable[] = {
		{ "Normal (read)",         ReadMode.Normal },
		{ "Disabled (don't read)", ReadMode.Disabled }
	}

	m_readModeProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Read mode",
		"Specify the HID input report read mode",
		readModeOptionTable,
		countof(readModeOptionTable)
	);

	m_bufferPropertySet.createPropertiesGrouped(
		m_pluginHost.m_propertyGrid,,,
		ui.BufferPropertySet.PropertyId.ReadBlockSize |
		ui.BufferPropertySet.PropertyId.ReadBufferSize
	);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("File stream");

	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();

	ui.refreshHidDeviceListUi(m_deviceCombo, m_deviceProp);
	m_uiReactor.start();
}

bool errorcode HidSession.open() {
	close();
	startSession();

	io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
	io.HidDeviceOptions options = io.HidDeviceOptions.NonBlocking;
	if (m_readModeProp.m_value == ReadMode.Disabled)
		options |= io.HidDeviceOptions.NoReadThread;

	m_device.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_device.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_device.m_options = options;
	m_device.open(desc.m_path);

	m_deviceInfoBuffer.setSize(sizeof(HidDeviceInfoParams));
	HidDeviceInfoParams* params = (HidDeviceInfoParams*)m_deviceInfoBuffer.m_p;
	params.m_releaseNumber = desc.m_releaseNumber;
	params.m_vendorId = desc.m_vendorId;
	params.m_productId = desc.m_productId;
	params.m_usagePage = desc.m_usagePage;
	params.m_usage = desc.m_usage;
	params.m_interfaceId = desc.m_interfaceId;
	params.m_busType = desc.m_busType;

	m_deviceInfoBuffer.append(desc.m_path);
	m_deviceInfoBuffer.append(0);
	m_deviceInfoBuffer.append(desc.m_manufacturer);
	m_deviceInfoBuffer.append(0);
	m_deviceInfoBuffer.append(desc.m_product);
	m_deviceInfoBuffer.append(0);
	m_deviceInfoBuffer.append(desc.m_serialNumber);
	m_deviceInfoBuffer.append(0);
	m_logWriter.write(HidLogRecordCode.Open, m_deviceInfoBuffer.m_p, m_deviceInfoBuffer.m_size);

	m_rdBuffer.setSize(4096);
	size_t size = m_device.getReportDescriptor(m_rdBuffer.m_p, m_rdBuffer.m_size);
	m_rdBuffer.setSize(size);
	m_rd.parse(io.g_hidDb, m_rdBuffer.m_p, size);
	m_logWriter.write(HidLogRecordCode.ReportDescriptor, m_rdBuffer.m_p, size);

	m_reportWriter.reset();
	m_reportUi.createReportForms();
	m_state = State.Opened;
	m_sessionConnectionState = SessionConnectionState.Connected;
	m_isTransmitEnabled = true;

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

catch:
	m_logWriter.writeLastError(HidLogRecordCode.OpenError);
	m_device.close();
	return false;
}

void HidSession.close() {
	if (m_state != State.Opened)
		return;

	m_logWriter.write(HidLogRecordCode.Close);
	m_device.close();
	m_rd.clear();
	m_reportUi.clear();
	m_state = State.Closed;
	m_sessionConnectionState = SessionConnectionState.Disconnected;
	m_isTransmitEnabled = false;
	m_syncId++;
	m_stdSessionInfoSet.endSession();
}

void HidSession.waitHidDeviceEvents() {
	m_device.wait(
		io.HidDeviceEvents.IoError |
		io.HidDeviceEvents.ReadBufferFull |
		io.HidDeviceEvents.IncomingData,
		onHidDeviceEvent~(++m_syncId) @ m_pluginHost.m_mainThreadScheduler
	);
}

void HidSession.load(doc.Storage* storage) {
	uint_t vid = storage.readInt("deviceVid");
	uint_t pid = storage.readInt("devicePid");
	char const* path = storage.readString("devicePath");

	size_t index = ui.findHidDeviceComboIndex(m_deviceCombo, vid, pid, path);
	if (index != -1)
		m_deviceCombo.m_currentIndex = index;

	m_readModeProp.m_value = storage.readInt("readMode", ReadMode.Normal);
	m_bufferPropertySet.load(storage);
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

void HidSession.save(doc.Storage* storage) {
	io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
	if (desc) {
		storage.writeInt("deviceVid", desc.m_vendorId);
		storage.writeInt("devicePid", desc.m_productId);
		storage.writeString("devicePath", desc.m_path);
	}

	storage.writeInt("readMode", m_readModeProp.m_value);
	m_bufferPropertySet.save(storage);
}

void HidSession.updateProperties() {
	m_deviceProp.m_currentIndex = m_deviceCombo.m_currentIndex;
}

bool errorcode HidSession.applyProperties() {
	m_deviceCombo.m_currentIndex = m_deviceProp.m_currentIndex;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);

	if (!m_device.m_isOpen)
		return true;

	m_device.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_device.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	return true;
}

void HidSession.restoreDefaultProperties() {
	m_bufferPropertySet.restoreDefaults();
	m_readModeProp.m_value = ReadMode.Normal;
}

size_t errorcode HidSession.transmit(
	void const* p,
	size_t size
) {
	if (!size)
		return 0;

	if (m_state != State.Opened) {
		std.setError("HID device is not opened");
		return -1; // don't throw here
	}

	uint_t reportId = *(uchar_t const*)p;
	io.HidReport const* report = m_rd.findReport(io.HidReportType.Output, reportId);
	if (!report) {
		std.setError($"HID output #%d not found"(reportId));
		throw;
	}

	if (size < report.m_size + 1) {
		std.setError($"Incomplete HID output report (expected 1 byte for report ID followed %d bytes of report fields)"(report.m_size));
		throw;
	}

	size_t result = m_device.write(p, report.m_size + 1);

	m_reportWriter.writeReport(
		HidLogRecordCode.OutputReport,
		report,
		p + 1,
		report.m_size
	);

	m_pluginHost.m_txRxStats.addTx(report.m_size);
	return result;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
	return -1;
}

void HidSession.onHidDeviceEvent(
	uint_t syncId,
	io.HidDeviceEvents triggeredEvents
) {
	if (syncId != m_syncId)
		return;

	if (triggeredEvents & io.HidDeviceEvents.IoError) {
		m_logWriter.write(log.StdRecordCode.Error, m_device.m_ioError);
		close();
		return;
	}

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

	if (triggeredEvents & io.HidDeviceEvents.IncomingData) {
		size_t result = m_device.read(m_readBuffer.m_p, m_readBuffer.m_size);
		if (result) {
			uint_t reportId = 0;
			uchar_t const* p = m_readBuffer.m_p;
			if (m_rd.m_flags & io.HidRdFlags.HasReportId) {
				reportId = *p++;
				result--;
			}

			io.HidReport const* report = m_rd.findReport(io.HidReportType.Input, reportId);
			if (report)
				m_reportWriter.writeReport(HidLogRecordCode.InputReport, report, p, result);
			else
				m_reportWriter.writeUnknownReport(HidLogRecordCode.InputReport, reportId, p, result);

			m_pluginHost.m_txRxStats.addRx(result);
		}
	}

	waitHidDeviceEvents();
}

reactor HidSession.m_uiReactor {
	m_title = $"HID $(m_deviceCombo.m_editText)";
	m_actionTable[ActionId.Open].m_text = m_state ? "Close" : "Open";
	m_actionTable[ActionId.Open].m_icon = m_iconTable[m_state ? IconId.Close : IconId.Open];
	m_actionTable[ActionId.Info].m_isEnabled = m_state;
	m_readModeProp.m_isEnabled = !m_state;
	m_statusPaneTable[StatusPaneId.State].m_text = m_state ? "Opened" : "Closed";
}

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