//..............................................................................
//
// I2C/SPI Tap session
//

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

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

class I2cSpiTapSession: doc.Session
{
protected:
	enum Protocol
	{
		Spi,
		I2c,
	}

	enum Defaults
	{
		Proto           = Protocol.Spi,
		SpiMode         = 0,
		SpiEndianness   = true,
		SpiDataBits     = 8,
		ReadParallelism = 4,
		ReadBlockSize   = 64 * 1024,
		ReadBufferSize  = 1 * 1024 * 1024,

		UseLogCacheUpdateScrollRateLimit = true,
		LogCacheUpdateScrollRateLimit    = 16,
	}

	enum
	{
		CypressBufferSize         = 4 * 1024, // 4K (2K built-in in Cypress + 512 FPGA FIFO)
		GracefulStopWriteTimeout  = 1000,     // 1 sec
		CypressClearBufferTimeout = 200,      // 0.2 sec
		FpgaUploadBaseIndex       = 0x1000,
	}

	enum State
	{
		Closed,
		Uploading,
		Capturing,
	}

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

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

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

	enum StatusPaneId
	{
		State,
		_Count,
	}

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

	static char const* const m_fileNameTable[] =
	{
		__DIR__ "/firmware/spi.rpd",
		__DIR__ "/firmware/i2c.rpd",
	}

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;
	iox.FpgaUploader m_uploader;
	std.Buffer m_readBuffer;
	SpiDecoder m_spiDecoder;
	I2cDecoder m_i2cDecoder;
	Decoder* m_decoder;
	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_protocolCombo;

	// property grid

	ui.EnumProperty* m_deviceProp;
	ui.EnumProperty* m_protocolProp;
	ui.EnumProperty* m_spiModeProp;
	ui.EnumProperty* m_spiDataBitsProp;
	ui.EnumProperty* m_spiEndiannessProp;
	ui.BoolProperty* m_spiFlipMosiMisoProp;
	ui.BoolProperty* m_useLogUpdateRateLimitProp;
	ui.IntProperty* m_logUpdateRateLimitProp;
	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 capture();
	void stopCapture(bool isGraceful = true);

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

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

protected:
	void onCapture();

	void onFlipMosiMiso()
	{
		m_spiDecoder.m_isFlipMosiMiso = m_actionTable[ActionId.Flip].m_isChecked;
	}

	void onUpdateCompleted(bool result);

	void onInEndpointEvent(
		uint_t syncId,
		io.UsbEndpointEvents triggeredEvents
		);

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

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

I2cSpiTapSession.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));
}

void I2cSpiTapSession.createUi()
{
	static ui.EnumPropertyOption protocolTable[] =
	{
		{ "SPI", Protocol.Spi },
		{ "I2C", Protocol.I2c },
	}

	static ui.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 ui.EnumPropertyOption endiannessTable[] =
	{
		{ "Little-endian (LSBit first)", false },
		{ "Big-endian (MSBit first)",    true },
	}

	static ui.EnumPropertyOption dataBitsTable[] =
	{
		{ "4 bits",  4 },
		{ "8 bits",  8 },
		{ "12 bits", 12 },
		{ "16 bits", 16 },
	}

	// toolbar

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

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

	m_actionTable[ActionId.Refresh]  = createAction("Re-enumerate I2C/SPI tap devices", m_iconTable[IconId.Refresh]);
	m_actionTable[ActionId.Refresh].m_onTriggered += enumerateDevices;

	m_protocolCombo = m_pluginHost.m_toolBar.addComboBox(
		"Protocol:",
		protocolTable,
		countof(protocolTable)
		);

	m_actionTable[ActionId.Capture] = createAction("Capture", m_iconTable[IconId.Capture]);
	m_actionTable[ActionId.Capture].m_onTriggered += onCapture;
	m_actionTable[ActionId.Flip] = createAction("Flip SPI MOSI/MISO", m_iconTable[IconId.Flip]);
	m_actionTable[ActionId.Flip].m_isCheckable = true;
	m_actionTable[ActionId.Flip].m_onTriggered += onFlipMosiMiso;

	// status bar

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

	// property grid

	ui.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_spiDataBitsProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"SPI data bits",
		"SPI word size",
		dataBitsTable,
		countof(dataBitsTable)
		);

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

	m_spiFlipMosiMisoProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Flip SPI MOSI/MISO",
		"Flip interpretation of SPI MOSI/MISO lines as TX/RX"
		);

	group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Log update",
		"Limit log update rate for better performance"
		);

	m_useLogUpdateRateLimitProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Use update rate limit",
		"When ON, the engine only allows the specified number of scroll operations per second"
		);

	m_logUpdateRateLimitProp = m_pluginHost.m_propertyGrid.createIntProperty(
		group,,
		"Update rate limit",
		"Log cache update limit (in scroll operations per second)"
		);

	ui.BufferPropertySet.PropertyId ids =
		ui.BufferPropertySet.PropertyId.ReadParallelism |
		ui.BufferPropertySet.PropertyId.ReadBlockSize |
		ui.BufferPropertySet.PropertyId.ReadBufferSize |
		ui.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

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

	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_uiReactor.start();

	enumerateDevices();
	m_isTransmitEnabled = true;
}

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

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

void I2cSpiTapSession.enumerateDevices()
{
	m_deviceCombo.clear();

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

	ui.EnumPropertyOption* optionTable = new ui.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 Tap" (
			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 I2cSpiTapSession.capture()
{
	stopCapture();

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

	startSession();

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

	m_logWriter.write(I2cSpiTapLogRecordCode.DeviceOpened, m_deviceCombo.m_currentText);

	Protocol protocol = m_protocolCombo.m_currentData;
	return m_uploader.update(m_device, m_fileNameTable[protocol], FpgaUploadBaseIndex);

catch:
	m_logWriter.writeLastError(I2cSpiTapLogRecordCode.CaptureError);
	return false;
}

bool errorcode I2cSpiTapSession.startCapture()
{
	m_iface = m_device.claimInterface(0);
	m_outEndpoint = m_iface.openEndpoint(Usb.OutEndpoint);
	gracefulStop();

	// 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, true);
	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;
	m_inEndpoint.unsuspend();

	char startCode;

	switch (m_protocolCombo.m_currentData)
	{
	case Protocol.Spi:
		m_spiDecoder.m_dataBits = m_spiDataBitsProp.m_value;
		m_spiDecoder.m_isBigEndian = m_spiEndiannessProp.m_value;
		m_spiDecoder.m_isFlipMosiMiso = m_actionTable[ActionId.Flip].m_isChecked;
		m_decoder = m_spiDecoder;
		startCode = m_spiModeProp.m_value;
		startCode += m_spiEndiannessProp.m_value ? 0x70 : 0x80;
		break;

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

	m_decoder.reset();
	m_outEndpoint.write(&startCode, sizeof(startCode));
	m_logWriter.write(I2cSpiTapLogRecordCode.CaptureStarted);

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

catch:
	m_logWriter.writeLastError(I2cSpiTapLogRecordCode.CaptureError);
	return false;
}

bool errorcode I2cSpiTapSession.gracefulStop()
{
	static char stopCode = 0xaa;
	m_outEndpoint.write(&stopCode, sizeof(stopCode));

	io.UsbEndpointEvents events = m_outEndpoint.blockingWait(
		io.UsbEndpointEvents.WriteCompleted | io.UsbEndpointEvents.IoError,
		GracefulStopWriteTimeout
		);

	if (events & io.UsbEndpointEvents.IoError)
	{
		std.setError("Stop capture request failed");
		return false;
	}

	io.UsbEndpoint* ep = m_iface.openEndpoint(Usb.InEndpoint, true);
	ep.m_readParallelism = 1;
	ep.m_readBlockSize = Usb.PacketSize;
	ep.m_readBufferSize = CypressBufferSize;
	ep.m_options &= ~io.UsbEndpointOptions.KeepReadBlockSize;
	ep.unsuspend();

	for (;;)
	{
		ep.blockingWait(io.UsbEndpointEvents.IncomingData, CypressClearBufferTimeout);

		size_t size = ep.read(m_readBuffer.m_p, m_readBuffer.m_size);
		if (!size)
			break;

		// m_logWriter.write(log.StdRecordCode.PlainText, $"Cypress buffer clear received: $size");
		// m_logWriter.write(log.StdRecordCode.Bin, m_readBuffer.m_p, size);
	}

	ep.close();
	m_logWriter.write(I2cSpiTapLogRecordCode.CypressBufferCleared);
	return true;
}

void I2cSpiTapSession.stopCapture(bool isGraceful)
{
	if (m_outEndpoint)
	{
		if (isGraceful)
			try gracefulStop();

		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();
		m_logWriter.write(I2cSpiTapLogRecordCode.CaptureStopped);
		m_state = State.Closed;
	}

	m_uploader.cancel();
	m_syncId++;
}

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

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

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

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

	if (triggeredEvents & io.UsbEndpointEvents.IncomingData)
	{
		size_t size = m_inEndpoint.read(m_readBuffer.m_p, m_readBuffer.m_size);
		m_decoder.decode(sys.getTimestamp(), m_readBuffer.m_p, size);
	}

	waitInEndpointEvents();
}

void I2cSpiTapSession.load(doc.Storage* storage)
{
	m_deviceCombo.m_currentIndex  = storage.readInt("deviceIdx");
	m_protocolCombo.m_currentData = storage.readInt("protocol", Defaults.Proto);
	m_spiModeProp.m_value = storage.readInt("spiMode", Defaults.SpiMode);
	m_spiDataBitsProp.m_value = storage.readInt("spiDataBits", Defaults.SpiDataBits);
	m_spiEndiannessProp.m_value = storage.readInt("spiEndianness", Defaults.SpiEndianness);
	m_actionTable[ActionId.Flip].m_isChecked = storage.readBool("spiFlipMosiMiso", false);
	m_useLogUpdateRateLimitProp.m_value = storage.readBool("useLogUpdateRateLimitProp", Defaults.UseLogCacheUpdateScrollRateLimit);
	m_logUpdateRateLimitProp.m_value = storage.readInt("logUpdateRateLimitProp", Defaults.LogCacheUpdateScrollRateLimit);
	m_bufferPropertySet.load(storage);
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

void I2cSpiTapSession.save(doc.Storage* storage)
{
	storage.writeInt("deviceIdx", m_deviceCombo.m_currentIndex);
	storage.writeInt("protocol", m_protocolCombo.m_currentData);
	storage.writeInt("spiMode", m_spiModeProp.m_value);
	storage.writeInt("spiDataBits", m_spiDataBitsProp.m_value);
	storage.writeInt("spiEndianness", m_spiEndiannessProp.m_value);
	storage.writeBool("spiFlipMosiMiso", m_actionTable[ActionId.Flip].m_isChecked);
	storage.writeBool("useLogUpdateRateLimitProp", m_useLogUpdateRateLimitProp.m_value);
	storage.writeInt("logUpdateRateLimitProp", m_logUpdateRateLimitProp.m_value);
	m_bufferPropertySet.save(storage);
}

void I2cSpiTapSession.updateProperties()
{
	m_deviceProp.m_currentIndex = m_deviceCombo.m_currentIndex;
	m_protocolProp.m_currentIndex = m_protocolCombo.m_currentIndex;
	m_spiFlipMosiMisoProp.m_value = m_actionTable[ActionId.Flip].m_isChecked;
}

bool errorcode I2cSpiTapSession.applyProperties()
{
	m_deviceCombo.m_currentIndex = m_deviceProp.m_currentIndex;
	m_protocolCombo.m_currentIndex = m_protocolProp.m_currentIndex;
	m_actionTable[ActionId.Flip].m_isChecked = m_spiFlipMosiMisoProp.m_value;

	m_readBuffer.detach(); // drop buffer, so we re-allocate each time
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);

	m_pluginHost.m_log.m_cacheUpdateScrollRateLimit = m_useLogUpdateRateLimitProp.m_value ?
		m_logUpdateRateLimitProp.m_value :
		-1;

	m_spiDecoder.m_dataBits = m_spiDataBitsProp.m_value;
	m_spiDecoder.m_isBigEndian = m_spiEndiannessProp.m_value;
	m_spiDecoder.m_isFlipMosiMiso = m_actionTable[ActionId.Flip].m_isChecked;

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

void I2cSpiTapSession.restoreDefaultProperties()
{
	m_protocolProp.m_value = Defaults.Proto;
	m_spiModeProp.m_value = Defaults.SpiMode;
	m_spiDataBitsProp.m_value = Defaults.SpiDataBits;
	m_spiEndiannessProp.m_value = Defaults.SpiEndianness;
	m_spiFlipMosiMisoProp.m_value = false;
	m_useLogUpdateRateLimitProp.m_value = Defaults.UseLogCacheUpdateScrollRateLimit;
	m_logUpdateRateLimitProp.m_value = Defaults.LogCacheUpdateScrollRateLimit;
	m_bufferPropertySet.restoreDefaults();
}

size_t errorcode I2cSpiTapSession.transmit(
	void const* p,
	size_t size
	)
{
	uint64_t timestamp = sys.getTimestamp();

	if (m_protocolCombo.m_currentData == Protocol.I2c)
	{
		m_i2cDecoder.reset();
		m_i2cDecoder.decode(timestamp, p, size);
	}
	else
	{
		m_spiDecoder.m_dataBits = m_spiDataBitsProp.m_value;
		m_spiDecoder.m_isBigEndian = m_spiEndiannessProp.m_value;
		m_spiDecoder.m_isFlipMosiMiso  = m_actionTable[ActionId.Flip].m_isChecked;
		m_spiDecoder.reset();
		m_spiDecoder.decode(timestamp, p, size);
	}

	return size;
}

reactor I2cSpiTapSession.m_uiReactor
{
	m_title = $"$(m_protocolCombo.m_currentText) $(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_spiDataBitsProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
	m_spiEndiannessProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
	m_spiFlipMosiMisoProp.m_isEnabled = m_protocolProp.m_value == Protocol.Spi;
	m_actionTable[ActionId.Flip].m_isEnabled = m_protocolCombo.m_currentData == Protocol.Spi;
	m_logUpdateRateLimitProp.m_isEnabled = m_useLogUpdateRateLimitProp.m_value;
}

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