//..............................................................................
//
// I2C Sniffer session
//

import "io_usb.jncx"
import "io_BufferUi.jnc"
import "std_Buffer.jnc"
import "doc_Session.jnc"
import "gui_StdSessionInfo.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"

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

class I2cSpiSnifferSession: doc.Session
{
protected:
	enum Defaults
	{
		Proto           = Protocol.Spi,
		SpiMode         = 0,
		Endianness      = true,
		DataBits        = 8,
		ReadParallelism = 4,
		ReadBlockSize   = 64 * 1024,
		ReadBufferSize  = 1 * 1024 * 1024,

		LogCacheUpdateScrollRateLimit = 16,
	}

	enum State
	{
		Closed,
		Uploading,
		Capturing,
	}

	char const* m_statusText [] =
	{
		"Closed",
		"Uploading",
		"Capturing",
	}

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

	enum IconId
	{
		Capture,
		Stop,
		Refresh,
		_Count,
	}

	enum StatusPaneId
	{
		State,
		_Count,
	}

	enum Usb
	{
		VendorId    = 0x16d0,
		ProductId   = 0x0e27,
		OutEndpoint = 0x02,
		InEndpoint  = 0x86,
		PacketSize  = 512,
	}

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;
	Uploader m_uploader;
	std.Buffer m_readBuffer;
	SpiDecoder m_spiDecoder;
	I2cDecoder m_i2cDecoder;
	Decoder* m_decoder;
	uint_t m_syncId;

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

	// property grid

	gui.EnumProperty* m_deviceProp;
	gui.EnumProperty* m_protocolProp;
	gui.EnumProperty* m_spiModeProp;
	gui.EnumProperty* m_endiannessProp;
	gui.IntProperty* m_dataBitsProp;
	io.BufferPropertySet m_bufferPropertySet;

	// information grid

	gui.StdSessionInfoSet m_stdSessionInfoSet;
	log.ThroughputInfoSet m_throughputInfoSet;
	log.ChecksumInfoSet m_checksumInfoSet;

	// ui reactor

	reactor m_uiReactor;

public:
	construct (doc.PluginHost* pluginHost);

	bool errorcode capture ();
	void stopCapture ();

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

protected:
	onCapture ();
	onUpdateCompleted (bool result);

	onInEndpointEvent (
		uint_t syncId,
		io.UsbEndpointEvents triggeredEvents
		);

	onTest ();

	createUi ();
	enumerateDevices ();
	waitInEndpointEvents ();
	bool errorcode startCapture ();
}

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

I2cSpiSnifferSession.construct (doc.PluginHost* pluginHost)
{
	basetype.construct (pluginHost);
	m_uploader.construct (pluginHost);
	m_spiDecoder.construct (pluginHost.m_log.m_writer);
	m_i2cDecoder.construct (pluginHost.m_log.m_writer);

	m_uploader.m_onUpdateCompleted += onUpdateCompleted;
	m_decoder = m_spiDecoder;

	createUi ();

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

I2cSpiSnifferSession.createUi ()
{
	static gui.EnumPropertyOption protocolTable [] =
	{
		{ "SPI", Protocol.Spi },
		{ "I2C", Protocol.I2c },
	}

	static gui.EnumPropertyOption spiModeTable [] =
	{
		{ "SPI 0 (CPOL 0, CPHA 0)",  0 },
		{ "SPI 1 (CPOL 0, CPHA 1)",  1 },
		{ "SPI 2 (CPOL 1, CPHA 0)",  2 },
		{ "SPI 3 (CPOL 1, CPHA 1)",  3 },
	}

	static gui.EnumPropertyOption endiannessTable [] =
	{
		{ "Little-endian (LSBit first)", false },
		{ "Big-endian (MSBit first)",    true },
	}

	// toolbar

	m_iconTable [IconId.Capture] = new gui.Icon ("images/capture.png");
	m_iconTable [IconId.Stop] = new gui.Icon ("images/stop.png");
	m_iconTable [IconId.Refresh] = new gui.Icon ("images/refresh.png");

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

	m_actionTable [ActionId.Refresh]  = createAction ("Re-enumerate sniffers", m_iconTable [IconId.Refresh]);
	m_actionTable [ActionId.Refresh].m_onTriggered += enumerateDevices;

	m_actionTable [ActionId.Capture] = createAction("Capture", m_iconTable [IconId.Capture]);
	m_actionTable [ActionId.Capture].m_onTriggered += onCapture;

	// status bar

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

	// property grid

	gui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty (
		"Connection",
		"USB-specific connection settings"
		);

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

	m_protocolProp = m_pluginHost.m_propertyGrid.createEnumProperty (
		group,,
		"Protocol",
		"Specify hardware protocol",
		protocolTable,
		countof (protocolTable)
		);

	m_spiModeProp = m_pluginHost.m_propertyGrid.createEnumProperty (
		group,,
		"SPI mode",
		"SPI CPOL/CPHA mode",
		spiModeTable,
		countof (spiModeTable)
		);

	m_endiannessProp = m_pluginHost.m_propertyGrid.createEnumProperty (
		group,,
		"Endianness",
		"SPI bit endianness",
		endiannessTable,
		countof (endiannessTable)
		);

	m_dataBitsProp = m_pluginHost.m_propertyGrid.createIntProperty (
		group,,
		"Data bits",
		"SPI word size"
		);

	m_dataBitsProp.m_minValue = 4;
	m_dataBitsProp.m_maxValue = 16;

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

	m_bufferPropertySet.m_defaultReadParallelism = Defaults.ReadParallelism;
	m_bufferPropertySet.m_defaultReadBlockSize = Defaults.ReadBlockSize;
	m_bufferPropertySet.m_defaultReadBufferSize = Defaults.ReadBufferSize;
	m_bufferPropertySet.createPropertiesGrouped (m_pluginHost.m_propertyGrid,,, ids);

	// information grid

	gui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup ("USB connection");

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

	m_uiReactor.start ();

	enumerateDevices ();
	m_isTransmitEnabled = true;
}

I2cSpiSnifferSession.onCapture ()
{
	if (m_state)
		stopCapture ();
	else
		try capture ();
}

I2cSpiSnifferSession.onUpdateCompleted (bool result)
{
	if (result)
		try startCapture ();
	else
		stopCapture ();
}

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

	io.UsbDevice* const* deviceArray = io.createUsbDeviceArray ();
	size_t totalCount = dynamic countof (deviceArray);
	size_t actualCount = 0;

	gui.EnumPropertyOption* optionTable = new gui.EnumPropertyOption [totalCount];

	for (size_t i = 0; i < totalCount; i++)
	{
		io.UsbDevice* device = deviceArray [i];
		io.UsbDeviceDesc const* deviceDesc = device.m_deviceDesc;

		if (deviceDesc.m_vendorId != Usb.VendorId ||
			deviceDesc.m_productId != Usb.ProductId)
			continue;

		char const* text = $"@%(1;03d):%(2;03d) IO Ninja I2C/SPI Sniffer" (
			device.m_bus,
			device.m_address
			);

		m_deviceCombo.addItem (text, device);

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

	m_deviceProp.setOptions (optionTable, actualCount);
}

bool errorcode I2cSpiSnifferSession.capture ()
{
	stopCapture ();

	if (m_deviceCombo.m_currentIndex == -1)
		return true;

	startSession ();

	m_device = m_deviceCombo.m_currentData;
	m_device.open ();

	writeLogString (I2cSpiSnifferLogRecordCode.DeviceOpened, m_deviceCombo.m_currentText);

	return m_uploader.update (m_device, m_protocolProp.m_value);

catch:
	writeLogLastError (I2cSpiSnifferLogRecordCode.CaptureError);
	return false;
}

bool errorcode I2cSpiSnifferSession.startCapture ()
{
	m_iface = m_device.claimInterface (0);
	writeLog (I2cSpiSnifferLogRecordCode.InterfaceClaimed);

	m_outEndpoint = m_iface.openEndpoint (Usb.OutEndpoint);
	writeLog (I2cSpiSnifferLogRecordCode.OutEndpointOpened);

	// align read block on 512 bytes

	size_t readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	readBlockSize = (readBlockSize + Usb.PacketSize - 1) & ~(Usb.PacketSize - 1);

	m_inEndpoint = m_iface.openEndpoint (Usb.InEndpoint);
	m_inEndpoint.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
	m_inEndpoint.m_readBlockSize = readBlockSize;
	m_inEndpoint.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
	m_inEndpoint.m_options &= ~io.UsbEndpointOptions.KeepReadBlockSize;
	writeLog (I2cSpiSnifferLogRecordCode.InEndpointOpened);

	char startCode;

	switch (m_protocolProp.m_value)
	{
	case Protocol.Spi:
		m_spiDecoder.m_isBigEndian = m_endiannessProp.m_value;
		m_spiDecoder.m_dataBits = 8;
		m_decoder = m_spiDecoder;
		startCode = m_spiModeProp.m_value;
		startCode += m_endiannessProp.m_value ? 0x70 : 0x80;
		break;

	case Protocol.I2c:
		m_decoder = m_i2cDecoder;
		startCode = 0x55;
		break;
	}

	m_decoder.reset ();

	m_outEndpoint.write (&startCode, sizeof (startCode));
	writeLog (I2cSpiSnifferLogRecordCode.CaptureStarted);

	waitInEndpointEvents ();
	m_stdSessionInfoSet.startSession (m_pluginHost.m_mainThreadScheduler);
	m_state = State.Capturing;
	return true;

catch:
	writeLogLastError (I2cSpiSnifferLogRecordCode.CaptureError);
	return false;
}

I2cSpiSnifferSession.stopCapture ()
{
	if (m_outEndpoint)
	{
		static char stopCode = 0xaa;
		try m_outEndpoint.write (&stopCode, sizeof (stopCode));
		m_outEndpoint.blockingWait (io.UsbEndpointEvents.WriteCompleted);
		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_stdSessionInfoSet.endSession ();
		writeLog (I2cSpiSnifferLogRecordCode.CaptureStopped);
		m_state = State.Closed;
	}

	m_uploader.cancel ();
	m_syncId++;
}

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

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

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

	if (triggeredEvents & io.UsbEndpointEvents.IoError)
	{
		writeLogError (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_decoder.decode (m_readBuffer.m_p, size);
	}

	waitInEndpointEvents ();
}

I2cSpiSnifferSession.load (doc.Storage* storage)
{
	m_deviceCombo.m_currentIndex  = storage.readInt ("deviceIdx");
	m_protocolProp.m_value = storage.readInt ("protocol", Defaults.Proto);
	m_spiModeProp.m_value = storage.readInt ("spiMode", Defaults.SpiMode);
	m_endiannessProp.m_value = storage.readInt ("endianness", Defaults.Endianness);
	m_dataBitsProp.m_value = storage.readInt ("dataBits", Defaults.DataBits);
	m_bufferPropertySet.load (storage);
	m_readBuffer.setSize (m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

I2cSpiSnifferSession.save (doc.Storage* storage)
{
	storage.writeInt ("deviceIdx", (int) m_deviceCombo.m_currentIndex);
	storage.writeInt ("protocol", m_protocolProp.m_value);
	storage.writeInt ("spiMode", m_spiModeProp.m_value);
	storage.writeInt ("endianness", m_endiannessProp.m_value);
	storage.writeInt ("dataBits", m_dataBitsProp.m_value);
	m_bufferPropertySet.save (storage);
}

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

bool errorcode I2cSpiSnifferSession.applyProperties ()
{
	m_deviceCombo.m_currentIndex = m_deviceProp.m_currentIndex;
	m_readBuffer.detach (); // drop buffer, so we re-allocate each time
	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;
	return true;
}

I2cSpiSnifferSession.restoreDefaultProperties ()
{
	m_protocolProp.m_value = Defaults.Proto;
	m_spiModeProp.m_value = Defaults.SpiMode;
	m_endiannessProp.m_value = Defaults.Endianness;
	m_dataBitsProp.m_value = Defaults.DataBits;
	m_bufferPropertySet.restoreDefaults ();
}

reactor I2cSpiSnifferSession.m_uiReactor
{
	m_title = $"$(g_protocolNameTable [m_protocolProp.m_value]) $(m_deviceCombo.m_currentText)";
	m_actionTable [ActionId.Capture].m_text = m_state ? "Close Device" : "Open Device";
	m_actionTable [ActionId.Capture].m_icon = m_iconTable [m_state ? IconId.Stop : IconId.Capture];
	m_statusPaneTable [StatusPaneId.State].m_text = m_statusText [m_state];
	m_spiModeProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
	m_endiannessProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
	m_dataBitsProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
}

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