//..............................................................................
//
// Hardware Serial Monitor session
//

import "doc_Session.jnc"
import "io_usb.jncx"
import "io_UsbDb.jnc"
import "ui_BufferPropertySet.jnc"
import "ui_StdSessionInfoSet.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"
import "std_Buffer.jnc"

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

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

	static char const* m_stateStringTable[] =
	{
		"Closed",
		"Opened",
		"Capturing",
	}

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

	enum IconId
	{
		Refresh,
		Open,
		Close,
		Capture,
		StopCapture,
		_Count,
	}

	enum StatusPaneId
	{
		State,
		_Count,
	}

protected:
	// usb

	State bindable m_state;

	io.UsbDevice* m_device;
	io.UsbInterface* m_iface;
	io.UsbEndpoint* m_outEndpoint; // until we figure out how to use control ep 0
	io.UsbEndpoint* m_inEndpoint;
	std.Buffer m_readBuffer;
	uint64_t m_captureBaseTime;
	uint_t m_inEndpointSyncId;

	// current serial iface

	SerialIface* m_capturingSerialIface;

	Uart m_uart;
	Spi m_spi;
	I2c m_i2c;

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

	// property grid

	ui.EnumProperty* m_deviceProp;
	ui.EnumProperty* m_serialIfaceProp;
	ui.BoolProperty* m_showRawDataProp;
	ui.BufferPropertySet m_bufferPropertySet;

	// information grid

	ui.StdSessionInfoSet m_stdSessionInfoSet;
	ui.ThroughputInfoSet m_throughputInfoSet;
	ui.ChecksumInfoSet m_checksumInfoSet;
	ui.InformationValue* m_usbTotalSizeInfo;
	ui.InformationValue* m_usbThroughputInfo;

	// ui reactor

	reactor m_uiReactor;

	uint64_t m_usbTotalSize;
	uint64_t m_usbPrevTotalSize;
	uint64_t m_usbThroughputTimestamp;
	sys.Timer m_usbThroughputTimer;
	uint_t m_usbThroughputTimerSyncId;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode openDevice();
	void closeDevice();
	bool errorcode capture();
	bool errorcode stopCapture();

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

protected:
	onOpenDevice();
	onCapture();
	onUsbThroughputTimer(uint_t syncId);

	onInEndpointEvent(
		uint_t syncId,
		io.UsbEndpointEvents triggeredEvents
		);

	createUi();
	enumerateDevices();

	size_t errorcode writeOutEndpoint(
		void const* p,
		size_t size
		);

	waitInEndpointEvents();
	updateUsbThroughput(uint64_t timestamp = sys.getTimestamp());
}

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

HwSerialMonSession.construct(doc.PluginHost* pluginHost)
{
	basetype.construct(pluginHost);
	m_uart.construct(pluginHost);
	m_spi.construct(pluginHost);
	m_i2c.construct(pluginHost);

	createUi();

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

HwSerialMonSession.createUi()
{
	// toolbar

	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.Capture] = new ui.Icon("images/capture.png");
	m_iconTable[IconId.StopCapture] = new ui.Icon("images/pause.png");

	m_pluginHost.m_toolBar.addLabel("Sniffer:");
	m_deviceCombo = m_pluginHost.m_toolBar.addComboBox(-1);

	m_actionTable[ActionId.Refresh] = createAction("Re-enumerate Devices", m_iconTable [IconId.Refresh]);
	m_actionTable[ActionId.Refresh].m_onTriggered += enumerateDevices;
	m_actionTable[ActionId.Open] = createAction("Open Sniffer Device", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Open].m_onTriggered += onOpenDevice;
	m_actionTable[ActionId.Capture] = createAction("Capture", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Capture].m_onTriggered += onCapture;

	// status bar

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

	// property grid

	ui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Connection",
		"Logic analyzer connection settings"
		);

	m_deviceProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Sniffer Device",
		"Target USB device",
		null, 0
		);

	group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Decoding",
		"Serial interface decoding settings"
		);

	m_showRawDataProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Show Raw Data",
		"Show raw data packets from HW sniffer"
		);

	ui.EnumPropertyOption serialIfaceTable[] =
	{
		{ m_uart.m_name, m_uart },
		{ m_spi.m_name,  m_spi },
		{ m_i2c.m_name,  m_i2c },
	}

	m_serialIfaceProp  = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Serial Interface",
		"Serial interface type",
		serialIfaceTable,
		countof(serialIfaceTable)
		);

	m_uart.createProperties(group);
	m_spi.createProperties(group);
	m_i2c.createProperties(group);

	ui.BufferPropertySet.PropertyId ids =
		ui.BufferPropertySet.PropertyId.ReadParallelism |
		ui.BufferPropertySet.PropertyId.ReadBlockSize |
		ui.BufferPropertySet.PropertyId.ReadBufferSize |
		ui.BufferPropertySet.PropertyId.NotifyReadBufferFull;

	m_bufferPropertySet.createPropertiesGrouped(m_pluginHost.m_propertyGrid,,, ids);

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("Serial monitor");

	m_stdSessionInfoSet.createInformationValues(m_pluginHost.m_informationGrid, infoGroup);
	m_throughputInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);
	m_checksumInfoSet.createInformationValuesGrouped(m_pluginHost.m_informationGrid);

	m_usbTotalSizeInfo = m_pluginHost.m_informationGrid.createValue(infoGroup,,, "USB total bytes");
	m_usbTotalSizeInfo.m_isEnabled = false;
	m_usbThroughputInfo = m_pluginHost.m_informationGrid.createValue(infoGroup,,, "USB throughput");
	m_usbThroughputInfo.m_isEnabled = false;
	m_usbThroughputTimestamp = sys.getTimestamp();
	updateUsbThroughput(m_usbThroughputTimestamp);

	m_pluginHost.m_informationGrid.resizeNameColumnToContents();

	enumerateDevices();

	m_uiReactor.start();
}

HwSerialMonSession.onOpenDevice()
{
	if (m_state >= State.Opened)
		closeDevice();
	else
		try openDevice();
}

HwSerialMonSession.onCapture()
{
	if (m_state >= State.Capturing)
		stopCapture();
	else
		try capture();
}

HwSerialMonSession.enumerateDevices()
{
	m_deviceCombo.clear();

	io.UsbDevice* const* deviceArray = io.createUsbDeviceArray();
	size_t count = dynamic countof(deviceArray);

	ui.EnumPropertyOption* optionTable = new ui.EnumPropertyOption[count];

	size_t ftdiCount = 0;
	for (size_t i = 0; i < count; i++)
	{
		io.UsbDevice* device = deviceArray[i];
		io.UsbDeviceDesc const* deviceDesc = device.m_deviceDesc;
		io.UsbProduct const* product = g_usbDb.findProduct(deviceDesc.m_vendorId, deviceDesc.m_productId);
		if (!product)
			continue;

		char const* text = $"@%(1;03d):%(2;03d) %3 %4" (
			device.m_bus,
			device.m_address,
			product.m_vendor.m_name,
			product.m_name
			);

		m_deviceCombo.addItem(text, device);

		optionTable[ftdiCount].m_text = text;
		optionTable[ftdiCount].m_value = device;
		ftdiCount++;
	}

	m_deviceProp.setOptions(optionTable, ftdiCount);
}

bool errorcode HwSerialMonSession.openDevice()
{
	if (m_deviceCombo.m_currentIndex == -1)
		return false;

	closeDevice();
	startSession();

	m_device = m_deviceCombo.m_currentData;

	m_device.open();
	m_iface = m_device.claimInterface(0);
	m_outEndpoint = m_iface.openEndpoint(0x01);
	m_inEndpoint = m_iface.openEndpoint(0x81, true);
	m_inEndpoint.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_inEndpoint.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_inEndpoint.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_inEndpoint.unsuspend();

	io.UsbDeviceDesc const* deviceDesc = m_device.m_deviceDesc;

	DeviceParams params;
	params.m_bus = m_device.m_bus;
	params.m_address = m_device.m_address;
	params.m_productId = deviceDesc.m_productId;
	params.m_vendorId = deviceDesc.m_vendorId;

	m_logWriter.write(UsbLogRecordCode.DeviceOpened, &params, sizeof(params));
	m_state = State.Opened;
	return capture();

catch:
	m_logWriter.writeLastError(UsbLogRecordCode.DeviceOpenError);
	closeDevice();
	return false;
}

HwSerialMonSession.closeDevice()
{
	if (m_outEndpoint)
	{
		m_outEndpoint.close();
		m_outEndpoint = null;
	}

	if (m_inEndpoint)
	{
		m_inEndpoint.close();
		m_inEndpoint = null;
	}

	if (m_iface)
	{
		m_iface.release();
		m_iface = null;
	}

	if (m_device)
	{
		m_device.close();
		m_device = null;
	}

	if (m_state)
	{
		m_logWriter.write(UsbLogRecordCode.DeviceClosed);
		m_state = State.Closed;
	}
}

bool errorcode HwSerialMonSession.capture()
{
	static char packet[] = 0x"55 55 55 55";
	writeOutEndpoint(packet, sizeof(packet));
	m_captureBaseTime = sys.getTimestamp();

	m_capturingSerialIface = m_serialIfaceProp.m_value;
	m_capturingSerialIface.start();

	m_logWriter.write(HwSerialMonRecordCode.CaptureStarted, m_capturingSerialIface.m_name);
	m_state = State.Capturing;
	waitInEndpointEvents();
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);

	m_usbTotalSize = 0;
	m_usbPrevTotalSize = 0;
	m_usbTotalSizeInfo.m_isEnabled = true;
	m_usbThroughputInfo.m_isEnabled = true;
	m_usbThroughputTimer.startPeriodicTimer(onUsbThroughputTimer ~(++m_usbThroughputTimerSyncId) @ m_pluginHost.m_mainThreadScheduler, 1000);
	m_usbThroughputTimestamp = sys.getTimestamp();
	updateUsbThroughput(m_usbThroughputTimestamp);
	return true;
}

bool errorcode HwSerialMonSession.stopCapture()
{
	static char packet[] = 0x"aa aa aa aa";
	writeOutEndpoint(packet, sizeof(packet));

	m_capturingSerialIface.stop();
	m_capturingSerialIface = null;

	m_logWriter.write(HwSerialMonRecordCode.CaptureStopped);
	m_stdSessionInfoSet.endSession();
	m_usbThroughputTimer.stop();
	m_usbTotalSizeInfo.m_isEnabled = false;
	m_usbThroughputInfo.m_isEnabled = false;
	m_state = State.Opened;
	m_inEndpointSyncId++;
	m_usbThroughputTimerSyncId++;
	return true;
}

HwSerialMonSession.waitInEndpointEvents  ()
{
	io.UsbEndpointEvents eventMask =
		io.UsbEndpointEvents.IoError |
		io.UsbEndpointEvents.ReadBufferFull |
		io.UsbEndpointEvents.IncomingData;

	m_inEndpoint.wait(eventMask, onInEndpointEvent ~(++m_inEndpointSyncId) @ m_pluginHost.m_mainThreadScheduler);
}

HwSerialMonSession.load(doc.Storage* storage)
{
	m_deviceCombo.m_currentIndex  = storage.readInt("deviceIdx");
	m_showRawDataProp.m_value = storage.readBool("showRawData");
	m_serialIfaceProp.m_currentIndex  = storage.readInt("serialInterface");
	m_uart.loadProperties(storage);
	m_spi.loadProperties(storage);
	m_bufferPropertySet.load(storage);
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

HwSerialMonSession.save(doc.Storage* storage)
{
	m_bufferPropertySet.save(storage);
	storage.writeInt("deviceIdx", m_deviceCombo.m_currentIndex);
	storage.writeBool("showRawData", m_showRawDataProp.m_value);
	storage.writeInt("serialInterface", m_serialIfaceProp.m_currentIndex);
	m_uart.saveProperties(storage);
	m_spi.saveProperties(storage);
}

HwSerialMonSession.updateProperties()
{
	m_deviceProp.m_currentIndex = m_deviceCombo.m_currentIndex;
}

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

	if (m_state != State.Capturing)
		return true;

	m_inEndpoint.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_inEndpoint.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	m_inEndpoint.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;

	m_capturingSerialIface.start(); // restart capturing with the new parameters
	return true;
}

HwSerialMonSession.restoreDefaultProperties()
{
	m_deviceProp.m_currentIndex = 0;
	m_serialIfaceProp.m_currentIndex = 0;

	m_uart.restoreDefaultProperties();
	m_spi.restoreDefaultProperties();
}

size_t errorcode HwSerialMonSession.writeOutEndpoint(
	void const* p,
	size_t size
	)
{
	size_t result = try m_outEndpoint.write(p, size);
	if (result == -1)
		m_logWriter.writeLastError(log.StdRecordCode.Error);

	return result;
}

HwSerialMonSession.onUsbThroughputTimer(uint_t syncId)
{
	if (syncId == m_usbThroughputTimerSyncId)
		updateUsbThroughput();
}

HwSerialMonSession.updateUsbThroughput(uint64_t timestamp)
{
	uint64_t throughput;

	if (timestamp > m_usbThroughputTimestamp)
	{
		uint64_t timeSpan = timestamp - m_usbThroughputTimestamp;
		throughput = (m_usbTotalSize - m_usbPrevTotalSize) * 10000000 / timeSpan;
		m_usbPrevTotalSize = m_usbTotalSize;
		m_usbThroughputTimestamp = timestamp;
	}

	m_usbTotalSizeInfo.m_value = $"$(formatIntegerWithThousandSep (m_usbTotalSize))";
	m_usbThroughputInfo.m_value = $"$(formatIntegerWithThousandSep (throughput))";
}

HwSerialMonSession.onInEndpointEvent(
	uint_t syncId,
	io.UsbEndpointEvents triggeredEvents
	)
{
	if (syncId != m_inEndpointSyncId)
		return;

	if (triggeredEvents & io.UsbEndpointEvents.IoError)
	{
		m_logWriter.write(log.StdRecordCode.Error, m_inEndpoint.m_ioError);
		stopCapture();
		return;
	}

	if (triggeredEvents & io.UsbEndpointEvents.IncomingData)
	{
		size_t size = m_inEndpoint.read(m_readBuffer.m_p, m_readBuffer.m_size);
		m_usbTotalSize += size;

		uint64_t const* p = (uint64_t const*) m_readBuffer.m_p;
		uint64_t const* end = p + size / sizeof(uint64_t);
		for (; p < end; p++)
		{
			uint64_t frame = *p;
			if (!frame)
				continue; // skip padding

			uint64_t timestamp = m_captureBaseTime + frame / 655360;

			if (m_showRawDataProp.m_value)
				m_logWriter.write(timestamp, HwSerialMonRecordCode.RawPacket, p, sizeof(uint64_t));

			m_capturingSerialIface.processSignal(timestamp, frame);
		}
	}

	waitInEndpointEvents();
}

reactor HwSerialMonSession.m_uiReactor
{
	m_title = $"HW Serial Mon $(m_deviceCombo.m_currentText)";
	m_actionTable[ActionId.Open].m_text = m_state ? "Close Sniffer Device" : "Open Sniffer Device";
	m_actionTable[ActionId.Open].m_icon = m_iconTable[m_state ? IconId.Close : IconId.Open];
	m_actionTable[ActionId.Capture].m_text = m_state == State.Capturing ? "Stop Capture" : "Capture";
	m_actionTable[ActionId.Capture].m_icon = m_iconTable[m_state == State.Capturing ? IconId.StopCapture : IconId.Capture];
	m_actionTable[ActionId.Capture].m_isEnabled = m_state;

	m_serialIfaceProp.m_isEnabled = m_state < State.Capturing;
	m_uart.m_groupProp.m_isEnabled = m_serialIfaceProp.m_value == m_uart;
	m_spi.m_groupProp.m_isEnabled = m_serialIfaceProp.m_value == m_spi;

	m_isTransmitEnabled = m_state;
	m_statusPaneTable[StatusPaneId.State].m_text = m_stateStringTable[m_state];
}

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

io.UsbDb g_usbDb;

construct()
{
	try g_usbDb.load("sniffers.ids");
}

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