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

import "io_usb.jncx"
import "io_UsbDb.jnc"
import "doc_Plugin.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,
	}

	enum
	{
		ReadBufferSize	= 8 * 1024,
		WriteBufferSize = 8 * 1024,
	}

	// this packet has to be sent to IF00:EP01 to control endpoints

	enum EpctlCode
	{
		SetReadBufferSize  = 1,
		SetWriteBufferSize = 2,
		Close              = 0,
		Close2             = 3,
	}

	struct EpctlParams
	{
		uint32_t m_seqId;
		uint8_t m_endpointId;
		uint8_t m_command;
		uint16_t m_padding;
		uint32_t m_size;
		uint32_t m_padding2;
		uint32_t m_padding3;
	}

	struct SignalEdge
	{
		bigendian uint64_t m_timestamp : 48;
		bigendian uint64_t m_lines     : 16;
	}

protected:
	// usb

	State bindable m_state;

	io.UsbDevice* m_device;
	io.UsbInterface* m_iface0;
	io.UsbInterface* m_iface1;
	io.UsbEndpoint* m_endpoint01;
	io.UsbEndpoint* m_endpoint02;
	io.UsbEndpoint* m_endpoint82;

	uint_t m_epctlSeqId;
	uint64_t m_captureBaseTime;

	// 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_serialIfaceProp;
	gui.BoolProperty* m_showRawDataProp;

	// current serial iface

	SerialIface* m_capturingSerialIface;

	Uart m_uart;
	Spi m_spi;
	I2c m_i2c;

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

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

	bool errorcode epctl (
		uint8_t endpointId,
		EpctlCode code,
		size_t size,
		uint_t timeout = 1000
		);

	onOpenDevice ();
	onCapture ();
	onEndpoint82Event (io.UsbEndpointEventParams const* params);

	createGui ();
	reactor m_uiReactor ();
}

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

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

	pluginHost.m_log.attachRepresenter (representHwSerialMonLog);

	m_state = State.Closed;
	m_epctlSeqId = sys.getTimestamp () & 0xff; // set somewhat random seq id

	createGui ();
}

HwSerialMonSession.createGui ()
{
	// toolbar

	m_iconTable [IconId.Refresh] = new gui.Icon ("images/refresh.png");
	m_iconTable [IconId.Open] = new gui.Icon ("images/open.png");
	m_iconTable [IconId.Close] = new gui.Icon ("images/close.png");
	m_iconTable [IconId.Capture] = new gui.Icon ("images/capture.png");
	m_iconTable [IconId.StopCapture] = new gui.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;

	// property grid

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

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

	gui.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 (
		"Serial Interface",
		"Serial interface type",
		serialIfaceTable,
		countof (serialIfaceTable)
		);

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

	// status bar

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

	enumerateDevices ();

	m_uiReactor.start ();
	m_state = State.Closed;
}

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

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

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

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

	gui.EnumPropertyOption* optionTable = new gui.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.epctl (
	uint8_t endpointId,
	HwSerialMonSession.EpctlCode code,
	size_t size,
	uint_t timeout
	)
{
	EpctlParams params;
	params.m_seqId = m_epctlSeqId++;
	params.m_endpointId = endpointId;
	params.m_command = code;
	params.m_size = size;

	m_endpoint01.write (&params, sizeof (params), timeout);
	return true;

catch:
	writeLogLastError (UsbRecordCode.IoError);
	return false;
}

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

	closeDevice ();
	startSession ();

	m_device = m_deviceCombo.m_currentData;

	io.UsbDeviceDesc const* deviceDesc = m_device.m_deviceDesc;

	m_device.open ();
	m_iface0 = m_device.claimInterface (0);
	m_iface1 = m_device.claimInterface (1);
	m_endpoint01 = m_iface0.openEndpoint (0x01);
	m_endpoint02 = m_iface1.openEndpoint (0x02);
	m_endpoint82 = m_iface1.openEndpoint (0x82);
	m_endpoint82.m_onEndpointEvent += onEndpoint82Event @ m_pluginHost.m_mainThreadScheduler;
	m_endpoint82.m_isEndpointEventEnabled = true;

	epctl (0x02, EpctlCode.SetWriteBufferSize, WriteBufferSize);
	epctl (0x82, EpctlCode.SetReadBufferSize, ReadBufferSize);

	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;

	writeLog (UsbRecordCode.DeviceOpened, &params, sizeof (params));
	m_state = State.Opened;
	return capture ();

catch:
	writeLogLastError (UsbRecordCode.DeviceOpenError);
	closeDevice ();
	return false;
}

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

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

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

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

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

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

	if (m_state)
	{
		writeLog (UsbRecordCode.DeviceClosed);
		m_state = State.Closed;
	}
}

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

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

	writeLogString (HwSerialMonRecordCode.CaptureStarted, m_capturingSerialIface.m_name);
	m_state = State.Capturing;
	return true;
}

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

	m_capturingSerialIface.stop ();
	m_capturingSerialIface = null;

	writeLog (HwSerialMonRecordCode.CaptureStopped);
	m_state = State.Opened;
	return true;
}

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

HwSerialMonSession.save (doc.Storage* 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;

	if (m_state == State.Capturing)
		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.writeEp02 (
	void const* p,
	size_t size
	)
{
	// it seems like it's not necessary to set write buffer size before
	// every write -- once after open is enough

	// epctl (0x02, EpctlCode.SetWriteBufferSize, WriteBufferSize);

	size_t result = try m_endpoint02.write (p, size, 1000); // currently -- 1 sec timeout
	if (result == -1)
		writeLogLastError (UsbRecordCode.IoError);

	return result;
}

HwSerialMonSession.onEndpoint82Event (io.UsbEndpointEventParams const* params)
{
	if (params.m_syncId != m_endpoint82.m_syncId)
		return;

	switch (params.m_eventCode)
	{
	case io.UsbEndpointEventCode.ReadyRead:
		static char buffer [ReadBufferSize]; // static is OK -- we read in main thread
		size_t size = m_endpoint82.read (buffer, sizeof (buffer));

		SignalEdge const* p = (SignalEdge const*) buffer;
		SignalEdge const* end = p + size / sizeof (SignalEdge);
		for (; p < end; p++)
		{
			uint64_t timestamp = m_captureBaseTime + p.m_timestamp / 10;

			if (m_showRawDataProp.m_value)
				m_pluginHost.m_log.m_writer.write (timestamp, HwSerialMonRecordCode.RawPacket, p, sizeof (SignalEdge));

			m_capturingSerialIface.processSignal (timestamp, p.m_lines);
		}

		epctl (0x82, EpctlCode.SetReadBufferSize, ReadBufferSize);
		break;

	case io.UsbEndpointEventCode.IoError:
		writeLogError (UsbRecordCode.IoError, params.m_error);
		break;
	}

catch:
	writeLogLastError (UsbRecordCode.IoError);
}

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 ("ftdi.ids");
}

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