//..............................................................................
//
// 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_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,
		SendReport,
		GetFeatureReport,
		_Count,
	}

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

	enum StatusPaneId {
		State,
		_Count,
	}

protected:
	// device

	State bindable m_state;
	io.HidDevice m_device;
	io.HidRd m_rd;
	HidLogReportWriter m_reportWriter;
	HidReportFieldEncoder** m_reportFieldEncoderArray;
	std.Buffer m_deviceInfoBuffer;
	std.Buffer m_rdBuffer;
	std.Buffer m_readBuffer;
	std.Buffer m_reportBuffer;
	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;
	ui.ComboBox* m_reportCombo;
	ui.ComboBox* m_transmitModeCombo;
	ui.PropertyGrid* m_reportPropertyGrid;

	// property grid

	ui.EnumProperty* m_deviceProp;
	ui.EnumProperty* m_readModeProp;
	ui.EnumProperty* m_transmitModeProp;
	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
	) {
		return transmit((HidTransmitMode)m_transmitModeCombo.m_currentData, p, size);
	}

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

	override bool errorcode dispatch(
		uint_t code,
		variant_t in,
		variant_t* out
	);

	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 onSendReport();
	void onGetFeatureReport();

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

	void createUi();
	void setupReportUi();
	void updateReportUi(io.HidReport const* report);
	void waitHidDeviceEvents();
}

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

HidSession.construct(doc.PluginHost* pluginHost) {
	basetype.construct(pluginHost);
	m_reportWriter.construct(pluginHost.m_log.m_writer);

	createUi();

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

void HidSession.createUi() {
	ui.ListItem readModeOptionTable[] = {
		{ "Normal (read)",         ReadMode.Normal },
		{ "Disabled (don't read)", ReadMode.Disabled }
	}

	ui.ListItem transmitModeOptionTable[] = {
		{ "Send as output report",  HidTransmitMode.OutputReport },
		{ "Send as feature report", HidTransmitMode.FeatureReport }
	}

	// 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_iconTable[IconId.Recv] = new ui.Icon("images/recv.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;

	// report form

	ui.VBoxLayout layout;
	layout.addLabel("HID output & feature reports:");
	m_reportCombo = new ui.ComboBox;
	layout.addWidget(m_reportCombo);

	m_reportPropertyGrid = new ui.PropertyGrid;
	layout.addWidget(m_reportPropertyGrid, 1);

	m_actionTable[ActionId.SendReport] = new ui.Action(m_iconTable[IconId.Send], "Send report");
	m_actionTable[ActionId.SendReport].m_onTriggered += onSendReport;
	m_actionTable[ActionId.GetFeatureReport] = new ui.Action(m_iconTable[IconId.Recv], "Get feature report");
	m_actionTable[ActionId.GetFeatureReport].m_onTriggered += onGetFeatureReport;

	ui.ToolBar reportToolBar;
	reportToolBar.addStretch();
	reportToolBar.setIconSize(73, 27); // images/send.png
	reportToolBar.addAction(m_actionTable[ActionId.GetFeatureReport]);
	reportToolBar.addAction(m_actionTable[ActionId.SendReport]);
	layout.addWidget(reportToolBar);

	ui.VBoxLayout subLayout;
	m_transmitModeCombo = new ui.ComboBox(transmitModeOptionTable, countof(transmitModeOptionTable));
	layout.addLine(); // to stress it applies to the transmit pane
	layout.addLabel("Transmit mode:");
	layout.addWidget(m_transmitModeCombo);

	m_pluginHost.createForm(layout, 1);

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

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

	m_transmitModeProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Transmit mode",
		"Specify how the 'Transmit' pane sends data to the HID device",
		transmitModeOptionTable,
		countof(transmitModeOptionTable)
	);

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

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

void HidSession.setupReportUi() {
	size_t outputReportCount = m_rd.m_reportCount[io.HidReportType.Output];
	size_t featureReportCount = m_rd.m_reportCount[io.HidReportType.Feature];
	size_t txReportCount = outputReportCount + featureReportCount;

	bool isEnabled = txReportCount != 0;

	m_isTransmitEnabled = isEnabled;
	m_reportCombo.m_isEnabled = isEnabled;
	m_reportPropertyGrid.m_isEnabled = isEnabled;
	m_actionTable[ActionId.SendReport].m_isEnabled = isEnabled;
	m_actionTable[ActionId.GetFeatureReport].m_isEnabled = isEnabled;

	if (!isEnabled) {
		m_transmitModeCombo.m_isEnabled = false;
		m_transmitModeProp.m_isEnabled = false;
		return;
	}

	ui.ComboItem* reportComboItems = new ui.ComboItem[txReportCount];

	for (size_t i = 0; i < outputReportCount; i++) {
		io.HidReport const* report = m_rd.m_reportArray[io.HidReportType.Output][i];
		reportComboItems[i].m_value = report;
		reportComboItems[i].m_text = report.m_reportId ?
			$"Output #%1"(report.m_reportId) :
			"Output";
	}

	for (size_t i = 0, j = outputReportCount; i < featureReportCount; i++, j++) {
		io.HidReport const* report = m_rd.m_reportArray[io.HidReportType.Feature][i];
		reportComboItems[j].m_value = report;
		reportComboItems[j].m_text = report.m_reportId ?
			$"Feature #%1"(report.m_reportId) :
			"Feature";
	}

	m_reportCombo.setItems(reportComboItems, txReportCount);

	bool hasBothKinds = outputReportCount && featureReportCount;
	m_transmitModeCombo.m_isEnabled = hasBothKinds;
	m_transmitModeProp.m_isEnabled = hasBothKinds;

	if (hasBothKinds)
		m_reportCombo.insertSeparator(outputReportCount);
	else {
		HidTransmitMode mode = featureReportCount ?
			HidTransmitMode.FeatureReport :
			HidTransmitMode.OutputReport;
		m_transmitModeCombo.m_currentData = mode;
		m_transmitModeProp.m_value = mode;
	}
}

void HidSession.updateReportUi(io.HidReport const* report) {
	m_reportPropertyGrid.clear();
	m_reportFieldEncoderArray = null;

	if (!report)
		return;

	// calculate the number of encoders
	ui.GroupProperty* groupProp;
	size_t encoderCount = 0;
	for (size_t i = 0; i < report.m_fieldCount; i++) {
		io.HidReportField const* field = report.m_fieldArray[i];
		if (!field.m_isPadding)
			encoderCount += field[io.HidRdItemId.ReportCount];
	}

	HidReportFieldEncoder** encoderArray = new HidReportFieldEncoder*[encoderCount];

	io.HidUsagePage const* usagePage;
	size_t encoderIdx = 0;
	for (size_t i = 0; i < report.m_fieldCount; i++) {
		io.HidReportField const* field = report.m_fieldArray[i];
		if (field.m_isPadding)
			continue;

		bool isUnsigned = field.m_isUnsigned;

		if (usagePage != field.m_usagePage) {
			usagePage = field.m_usagePage;
			groupProp = new ui.GroupProperty(usagePage.m_string);
			m_reportPropertyGrid.addProperty(groupProp);
		}

		size_t reportSize = field[io.HidRdItemId.ReportSize];
		size_t reportCount = field[io.HidRdItemId.ReportCount];

		bool hasIndex =
			reportCount > 1 &&
			(field.m_valueFlags & io.HidRdValueFlags.Variable) &&
			field.m_usageArray[0] == field.m_usageArray[1];

		for (size_t j = 0; j < reportCount; j++) {
			string_t text =
				!(field.m_valueFlags & io.HidRdValueFlags.Variable) ?
					$"[$j]:" :
				hasIndex ?
					$"%1 [%2]:"(usagePage.getUsageString(field.m_usageArray[j]), j) :
					$"%1:"(usagePage.getUsageString(field.m_usageArray[j]));

			if (reportSize == 1) {
				ui.BoolProperty* prop = new ui.BoolProperty(text);
				groupProp.addChildProperty(prop);
				encoderArray[encoderIdx++] = new HidReportBitEncoder(prop);
			} else {
				ui.StringProperty* prop = new ui.StringProperty(text);
				prop->m_value = "0";
				groupProp.addChildProperty(prop);
				encoderArray[encoderIdx++] = new HidReportIntEncoder(prop, reportSize, isUnsigned);
			}
		}
	}

	m_reportPropertyGrid.resizeNameColumnToContents();
	m_actionTable[ActionId.GetFeatureReport].m_isEnabled = report.m_reportType == io.HidReportType.Feature;
	m_reportFieldEncoderArray = encoderArray;
}

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(HidLogDeviceInfoParams));
	HidLogDeviceInfoParams* params = (HidLogDeviceInfoParams*)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_state = State.Opened;
	m_sessionConnectionState = SessionConnectionState.Connected;
	setupReportUi();
	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_reportCombo.clear();
	m_reportPropertyGrid.clear();
	m_transmitModeCombo.m_isEnabled = false;
	m_transmitModeProp.m_isEnabled = false;
	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) {
	string_t path = storage.readString("devicePath");
	size_t index = ui.findHidDeviceComboIndex(m_deviceCombo, path); // try path first
	if (index == -1) { // try VID:PID:usage
		uint_t vid = storage.readInt("deviceVid");
		uint_t pid = storage.readInt("devicePid");
		uint_t usagePage = storage.readInt("deviceUsagePage");
		uint_t usage = storage.readInt("deviceUsage");
		index = ui.findHidDeviceComboIndex(m_deviceCombo, vid, pid, usagePage, usage);
	}

	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.writeInt("deviceUsagePage", desc.m_usagePage);
		storage.writeInt("deviceUsage", desc.m_usage);
		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(
	HidTransmitMode mode,
	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
	}

	struct TransmitDetails {
		string_t m_description;
		io.HidReportType m_reportType;
		uint64_t m_recordCode;
	};

	static TransmitDetails detailTable[2] = {
		{
			"HID output report",
			io.HidReportType.Output,
			HidLogRecordCode.OutputReport,
		},
		{
			"HID feature report",
			io.HidReportType.Feature,
			HidLogRecordCode.SendFeatureReport
		},
	}

	TransmitDetails details = detailTable[mode];

	uint_t reportId = *(uchar_t const*)p;
	io.HidReport const* report = m_rd.findReport(details.m_reportType, reportId);
	if (!report) {
		std.setError($"%1 #%2 not found"(details.m_description, reportId));
		return -1;
	}

	size_t writeSize = report.m_size + 1; // plus report ID
	if (size < writeSize) {
		std.setError(
			$"Incomplete %1 (expected 1 byte for report ID followed %2 bytes of report fields)"(
				details.m_description,
				report.m_size
			)
		);
		return -1;
	}

	size_t result = details.m_reportType == io.HidReportType.Feature ?
		m_device.sendFeatureReport(p, writeSize) :
		m_device.write(p, writeSize);

	m_reportWriter.writeReport(details.m_recordCode, report, p, writeSize);
	m_pluginHost.m_txRxStats.addTx(writeSize);
	return result;
}

bool errorcode HidSession.dispatch(
	uint_t code,
	variant_t in,
	variant_t* out
) {
	switch (code) {
	case HidDispatchCode.GetDeviceVid:
		io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
		*out = desc ? desc.m_vendorId : -1;
		break;

	case HidDispatchCode.GetDevicePid:
		io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
		*out = desc ? desc.m_productId : -1;
		break;

	case HidDispatchCode.GetDeviceUsagePage:
		io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
		*out = desc ? desc.m_usagePage : -1;
		break;

	case HidDispatchCode.GetDeviceUsage:
		io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
		*out = desc ? desc.m_usage : -1;
		break;

	case HidDispatchCode.SetDeviceVidPid:
		uint32_t vidPid = (uint_t)in;
		size_t i = ui.findHidDeviceComboIndex(
			m_deviceCombo,
			(vidPid >> 16) & 0xffff,
			vidPid & 0xffff
		);

		if (i == -1) {
			std.setError("can't find device with specified VID:PID");
			return false;
		}

		m_deviceCombo.m_currentIndex = i;
		break;

	case HidDispatchCode.SetDeviceVidPidUsage:
		uint64_t vidPidUsage = (uint64_t)in;
		size_t i = ui.findHidDeviceComboIndex(
			m_deviceCombo,
			(vidPidUsage >> 16) & 0xffff,
			vidPidUsage & 0xffff,
			(vidPidUsage >> 48) & 0xffff,
			(vidPidUsage >> 32) & 0xffff
		);

		if (i == -1) {
			std.setError($"can't find device with specified VID:PID and usage $(vidPidUsage; 0x%llx)");
			return false;
		}

		m_deviceCombo.m_currentIndex = i;
		break;

	case HidDispatchCode.GetDevicePath:
		io.HidDeviceDesc const* desc = m_deviceCombo.m_currentData;
		*out = desc ? desc.m_path : null;
		break;

	case HidDispatchCode.SetDevicePath:
		size_t i = ui.findHidDeviceComboIndex(m_deviceCombo, (string_t)in);
		if (i == -1) {
			std.setError("can't find device with specified path");
			return false;
		}

		m_deviceCombo.m_currentIndex = i;
		break;

	case HidDispatchCode.GetTransmitMode:
		*out = m_transmitModeCombo.m_currentData;
		break;

	case HidDispatchCode.SetTransmitMode:
		m_transmitModeCombo.m_currentData = in;
		break;

	case HidDispatchCode.OpenDevice:
		return open();

	case HidDispatchCode.CloseDevice:
		close();
		break;

	case HidDispatchCode.SendOutputReport:
		string_t data = in;
		transmit(HidTransmitMode.OutputReport, data.m_p, data.m_length);
		break;

	case HidDispatchCode.SendFeatureReport:
		string_t data = in;
		transmit(HidTransmitMode.FeatureReport, data.m_p, data.m_length);
		break;

	default:
		std.setError("unsupported dispatch code");
		return false;
	}

	return true;
}

void HidSession.onSendReport() {
	io.HidReport const* report = (io.HidReport const*)m_reportCombo.m_currentData;
	if (!report)
		return;

	size_t writeSize = report.m_size + 1; // plus report ID
	m_reportBuffer.setSize(report.m_size + sizeof(uint64_t)); // plus padding for encoder
	memset(m_reportBuffer.m_p, 0, writeSize);

	// encode

	m_reportBuffer[0] = report.m_reportId; // report ID in the first byte
	size_t bitOffset = 8; // report ID
	size_t encoderIdx = 0;
	for (size_t i = 0; i < report.m_fieldCount; i++) {
		io.HidReportField const* field = report.m_fieldArray[i];
		if (field.m_isPadding) {
			bitOffset += field.m_bitCount;
			continue;
		}

		size_t reportSize = field[io.HidRdItemId.ReportSize];
		size_t reportCount = field[io.HidRdItemId.ReportCount];
		for (size_t j = 0; j < reportCount; j++) {
			m_reportFieldEncoderArray[encoderIdx++].encode(m_reportBuffer.m_p, bitOffset);
			bitOffset += reportSize;
		}
	}

	uint64_t recordCode;

	if (report.m_reportType == io.HidReportType.Feature) {
		m_device.sendFeatureReport(m_reportBuffer.m_p, writeSize);
		recordCode = HidLogRecordCode.SendFeatureReport;
	} else {
		m_device.write(m_reportBuffer.m_p, writeSize);
		recordCode = HidLogRecordCode.OutputReport;
	}

	m_reportWriter.writeReport(recordCode, report, m_reportBuffer.m_p, writeSize);
	m_pluginHost.m_txRxStats.addTx(writeSize);

catch:
	m_logWriter.write(log.StdRecordCode.Error, std.getLastError());
}

void HidSession.onGetFeatureReport() {
	io.HidReport const* report = (io.HidReport const*)m_reportCombo.m_currentData;
	if (!report)
		return;

	size_t readSize = report.m_size + 1; // plus report ID;
	m_reportBuffer.setSize(report.m_size + sizeof(uint64_t)); // plus padding for decoder
	memset(m_reportBuffer.m_p, 0, readSize);
	m_reportBuffer[0] = report.m_reportId; // report ID in the first byte

	m_device.getFeatureReport(m_reportBuffer.m_p, readSize);
	m_reportWriter.writeReport(HidLogRecordCode.GetFeatureReport, report, m_reportBuffer.m_p, readSize);
	m_pluginHost.m_txRxStats.addRx(readSize);

	// decode

	void* p = m_reportBuffer.m_p + 1;
	size_t bitOffset = 0;
	size_t encoderIdx = 0;
	for (size_t i = 0; i < report.m_fieldCount; i++) {
		io.HidReportField const* field = report.m_fieldArray[i];
		if (field.m_isPadding) {
			bitOffset += field.m_bitCount;
			continue;
		}

		size_t reportSize = field[io.HidRdItemId.ReportSize];
		size_t reportCount = field[io.HidRdItemId.ReportCount];
		for (size_t j = 0; j < reportCount; j++) {
			m_reportFieldEncoderArray[encoderIdx++].decode(p, bitOffset);
			bitOffset += reportSize;
		}
	}

catch:
	m_logWriter.write(log.StdRecordCode.Error, std.getLastError());
}

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) {
			uint64_t recordCode;
			uint_t reportId;
			uchar_t const* p = m_readBuffer.m_p;
			if (m_rd.m_flags & io.HidRdFlags.HasReportId) {
				reportId = *p;
				recordCode = HidLogRecordCode.InputReport;
			} else {
				reportId = 0;
				recordCode = HidLogRecordCode.InputReportNoId;
			}

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

			m_pluginHost.m_txRxStats.addRx(result);
		}
	}

	waitHidDeviceEvents();
}

reactor HidSession.m_uiReactor {
	m_title = $"HID $(m_deviceCombo.m_currentText)";
	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";
	updateReportUi((io.HidReport const*)m_reportCombo.m_currentData);
}

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