//..............................................................................
//
// USB Data Endpoint terminal session
//

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

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

class UsbDataEndpointSession: doc.Session
{
protected:
	enum Defaults
	{
		ReadParallelism = 4,
		ReadBlockSize   = 32 * 1024,
		ReadBufferSize  = 512 * 1024,
		ReadTimeout     = 1000,
		UseReadTimeout  = false,
		AutoDetachKernelDriver = true,
	}

	enum State
	{
		Closed,
		Opened,
		Claimed,
		Reading,
	}

	enum ActionId
	{
		Refresh,
		Open,
		Claim,
		Read,
		_Count,
	}

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

	enum StatusPaneId
	{
		State,
		_Count,
	}

protected:
	// usb

	State bindable m_state;

	io.UsbDevice* m_device;
	io.UsbInterface* m_interface;
	io.UsbEndpoint* m_outEndpoint;
	io.UsbEndpoint* m_inEndpoint;
	std.Buffer m_readBuffer;
	uint_t m_outSyncId;
	uint_t m_inSyncId;

	// 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_interfaceCombo;
	ui.ComboBox* m_outEndpointCombo;
	ui.ComboBox* m_inEndpointCombo;

	// property grid

	ui.EnumProperty* m_deviceProp;
	ui.EnumProperty* m_interfaceProp;
	ui.EnumProperty* m_inEndpointProp;
	ui.EnumProperty* m_outEndpointProp;
	ui.BoolProperty* m_autoDetachKernelDriverProp;
	ui.BoolProperty* m_useReadTimeoutProp;
	ui.IntProperty* m_readTimeoutProp;
	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 openDevice();
	void closeDevice();
	bool claimInterface();
	void releaseInterface();
	bool startReading();
	void stopReading();

	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 onEnumerateDevices()
	{
		ui.refreshUsbDeviceListUi(m_deviceCombo, m_deviceProp);
	}

	void onOpenDevice();
	void onClaimInterface();
	void onReadEndpoint();

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

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

	void configureInEndpoint();

	void createUi();
	void waitOutEndpointEvents();
	void waitInEndpointEvents();
}

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

UsbDataEndpointSession.construct(doc.PluginHost* pluginHost)
{
	basetype.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);
}

void UsbDataEndpointSession.createUi()
{
	// toolbar

	m_iconTable[IconId.Open] = new ui.Icon("images/open.png");
	m_iconTable[IconId.Close] = new ui.Icon("images/close.png");
	m_iconTable[IconId.Refresh] = new ui.Icon("images/refresh.png");

	m_pluginHost.m_toolBar.addLabel("Device:");
	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 += onEnumerateDevices;

	m_actionTable[ActionId.Open] = createAction("Open device", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Open].m_onTriggered += onOpenDevice;

	m_pluginHost.m_toolBar.addLabel("Iface:");
	m_interfaceCombo = m_pluginHost.m_toolBar.addComboBox();

	m_actionTable[ActionId.Claim] = createAction("Claim interface", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Claim].m_onTriggered += onClaimInterface;

	m_pluginHost.m_toolBar.addLabel("Out:");
	m_outEndpointCombo = m_pluginHost.m_toolBar.addComboBox();

	m_pluginHost.m_toolBar.addLabel("In:");
	m_inEndpointCombo = m_pluginHost.m_toolBar.addComboBox();

	m_actionTable[ActionId.Read] = createAction("Start reading from IN endpoint", m_iconTable [IconId.Open]);
	m_actionTable[ActionId.Read].m_onTriggered += onReadEndpoint;

	// 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_autoDetachKernelDriverProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Auto-detach",
		"Auto detach kernel driver (if present)"
		);

	m_interfaceProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"Interface",
		"Target USB interface",
		null, 0
		);

	m_outEndpointProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"OUT endpoint",
		"Transmitting USB endpoint",
		null, 0
		);

	m_inEndpointProp = m_pluginHost.m_propertyGrid.createEnumProperty(
		group,,
		"IN endpoint",
		"Receiving USB endpoint",
		null, 0
		);

	group = m_pluginHost.m_propertyGrid.createGroupProperty(
		"Buffering",
		"Advanced buffering options affecting throughtput and compatibility"
		);

	m_useReadTimeoutProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		group,,
		"Use read timeout",
		"If turned OFF then USB read transfers only complete when read block is fully filled"
		);

	m_readTimeoutProp = m_pluginHost.m_propertyGrid.createIntProperty(
		group,,
		"Read timeout",
		"Specify timeouts for USB read transfers"
		);

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

	// information grid

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

	ui.refreshUsbDeviceListUi(m_deviceCombo, m_deviceProp);
}

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

void UsbDataEndpointSession.onClaimInterface()
{
	if (m_state >= State.Claimed)
		releaseInterface();
	else
		claimInterface();
}

void UsbDataEndpointSession.onReadEndpoint()
{
	if (m_state >= State.Reading)
		stopReading();
	else
		startReading();
}

bool UsbDataEndpointSession.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_interfaceCombo.clear();

	io.UsbConfigurationDesc const* confDesc = m_device.m_activeConfigurationDesc;
	ui.EnumPropertyOption* optionTable = new ui.EnumPropertyOption[confDesc.m_interfaceCount];

	for (size_t i = 0; i < confDesc.m_interfaceCount; i++)
	{
		io.UsbInterfaceDesc const* ifaceDesc = &confDesc.m_interfaceTable[i];
		char const* name =  $"$(ifaceDesc.m_interfaceId)";
		optionTable[i].m_text = name;
		optionTable[i].m_value = ifaceDesc;

		m_interfaceCombo.addItem(name, ifaceDesc);

		io.UsbInterfaceDesc const* ifaceAltDesc = ifaceDesc.m_nextAltSettingInterface;
		for (; ifaceAltDesc; ifaceAltDesc = ifaceAltDesc.m_nextAltSettingInterface)
		{
			m_interfaceCombo.addItem(
				$"  $(ifaceAltDesc.m_interfaceId):$(ifaceAltDesc.m_altSettingId)",
				ifaceAltDesc
				);
		}
	}

	m_interfaceProp.setOptions(optionTable, confDesc.m_interfaceCount);

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

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

void UsbDataEndpointSession.closeDevice()
{
	if (m_state == State.Closed)
		return;

	releaseInterface();
	m_device.close();
	m_device = null;

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

bool UsbDataEndpointSession.claimInterface()
{
	if (m_interfaceCombo.m_currentIndex == -1)
		return false;

	releaseInterface();

	io.UsbInterfaceDesc const* ifaceDesc = m_interfaceCombo.m_currentData;

	if (m_device.m_isKernelDriverActive[ifaceDesc.m_interfaceId] &&
		m_autoDetachKernelDriverProp.m_value)
	{
		m_device.detachKernelDriver(ifaceDesc.m_interfaceId);
		m_logWriter.write(UsbLogRecordCode.KernelDriverDetached, &ifaceDesc.m_interfaceId, sizeof(ifaceDesc.m_interfaceId));
	}

	m_interface = m_device.claimInterface(ifaceDesc.m_interfaceId, ifaceDesc.m_altSettingId);

	m_outEndpointCombo.clear();
	m_inEndpointCombo.clear();

	ui.EnumPropertyOption* outEnpointOptionTable = new ui.EnumPropertyOption[ifaceDesc.m_endpointCount];
	ui.EnumPropertyOption* inEnpointOptionTable = new ui.EnumPropertyOption[ifaceDesc.m_endpointCount];

	size_t outEndpointCount = 0;
	size_t inEndpointCount = 0;

	for (size_t i = 0; i < ifaceDesc.m_endpointCount; i++)
	{
		io.UsbEndpointDesc const* endpointDesc = &ifaceDesc.m_endpointTable[i];

		char const* name = $"$(endpointDesc.m_endpointId; 02x)";

		if (endpointDesc.m_endpointId & 0x80)
		{
			m_inEndpointCombo.addItem(name, endpointDesc);
			inEnpointOptionTable[inEndpointCount].m_text = name;
			inEnpointOptionTable[inEndpointCount].m_value = endpointDesc;
			inEndpointCount++;
		}
		else
		{
			m_outEndpointCombo.addItem(name, endpointDesc);
			outEnpointOptionTable[outEndpointCount].m_text = name;
			outEnpointOptionTable[outEndpointCount].m_value = endpointDesc;
			outEndpointCount++;
		}
	}

	m_outEndpointProp.setOptions(outEnpointOptionTable, outEndpointCount);
	m_inEndpointProp.setOptions(inEnpointOptionTable, inEndpointCount);

	InterfaceParams params;
	params.m_interfaceId = ifaceDesc.m_interfaceId;
	params.m_altSettingId = ifaceDesc.m_altSettingId;

	m_logWriter.write(UsbLogRecordCode.InterfaceClaimed, &params, sizeof(params));
	m_state = State.Claimed;
	m_stdSessionInfoSet.startSession(m_pluginHost.m_mainThreadScheduler);
	return true;

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

void UsbDataEndpointSession.releaseInterface()
{
	if (m_state <= State.Opened)
		return;

	stopReading();
	m_interface.release();
	m_interface = null;

	m_logWriter.writeLastError(UsbLogRecordCode.InterfaceReleased);
	m_state = State.Opened;
	m_inSyncId++;
	m_outSyncId++;
	m_stdSessionInfoSet.endSession();
}

bool UsbDataEndpointSession.startReading()
{
	if (m_inEndpointCombo.m_currentIndex == -1)
		return false;

	stopReading();

	io.UsbEndpointDesc const* endpointDesc = m_inEndpointCombo.m_currentData;

	m_inEndpoint = m_interface.openEndpoint(endpointDesc.m_endpointId, true);
	configureInEndpoint();
	m_inEndpoint.unsuspend();

	m_logWriter.write(UsbLogRecordCode.ReadingStarted, &endpointDesc.m_endpointId, sizeof(endpointDesc.m_endpointId));
	m_state = State.Reading;

	waitInEndpointEvents();
	return true;

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

void UsbDataEndpointSession.stopReading()
{
	if (m_state <= State.Claimed)
		return;

	m_inEndpoint.close();
	m_inEndpoint = null;

	m_logWriter.writeLastError(UsbLogRecordCode.ReadingStopped);
	m_state = State.Claimed;
	m_inSyncId++;
}

void UsbDataEndpointSession.configureInEndpoint()
{
	if (m_useReadTimeoutProp.m_value)
	{
		m_inEndpoint.m_transferTimeout = m_readTimeoutProp.m_value;
		m_inEndpoint.m_readParallelism = 1;
		m_inEndpoint.m_readBlockSize = m_bufferPropertySet.m_readBlockSizeProp.m_value;
	}
	else
	{
		m_inEndpoint.m_transferTimeout = -1;
		m_inEndpoint.m_readParallelism = m_bufferPropertySet.m_readParallelismProp.m_value;
		m_inEndpoint.m_readBlockSize = m_inEndpoint.m_endpointDesc.m_maxPacketSize; // if no timeout, we must read with the USB packet size
	}

	m_inEndpoint.m_readBufferSize = m_bufferPropertySet.m_readBufferSizeProp.m_value;
}

void UsbDataEndpointSession.waitOutEndpointEvents()
{
	io.UsbEndpointEvents eventMask = io.UsbEndpointEvents.IoError;

	if (!m_isTransmitEnabled)
		eventMask |= io.UsbEndpointEvents.WriteBufferReady;

	m_outEndpoint.wait(eventMask, onOutEndpointEvent~(++m_outSyncId) @ m_pluginHost.m_mainThreadScheduler);
}

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

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

void UsbDataEndpointSession.load(doc.Storage* storage)
{
	uint_t vid = storage.readInt("deviceVid");
	uint_t pid = storage.readInt("devicePid");
	uint_t bus = storage.readInt("deviceBus");
	uint_t address = storage.readInt("deviceAddress");

	size_t index = ui.findUsbDeviceComboIndex(m_deviceCombo, vid, pid, bus, address);
	if (index != -1)
		m_deviceCombo.m_currentIndex = index;

	m_autoDetachKernelDriverProp.m_value = storage.readBool("autoDetachKernelDriver", Defaults.AutoDetachKernelDriver);
	m_useReadTimeoutProp.m_value = storage.readBool("useReadTimeoutProp", Defaults.UseReadTimeout);
	m_readTimeoutProp.m_value = storage.readInt("readTimeoutProp", Defaults.ReadTimeout);
	m_bufferPropertySet.load(storage);
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

void UsbDataEndpointSession.save(doc.Storage* storage)
{
	m_bufferPropertySet.save(storage);

	io.UsbDevice* device = m_deviceCombo.m_currentData;
	if (device)
	{
		storage.writeInt("deviceVid", device.m_deviceDesc.m_vendorId);
		storage.writeInt("devicePid", device.m_deviceDesc.m_productId);
		storage.writeInt("deviceBus", device.m_bus);
		storage.writeInt("deviceAddress", device.m_address);
	}

	storage.writeBool("autoDetachKernelDriver", m_autoDetachKernelDriverProp.m_value);
	storage.writeBool("useReadTimeoutProp", m_useReadTimeoutProp.m_value);
	storage.writeInt("readTimeoutProp", m_readTimeoutProp.m_value);
	storage.writeBool("useReadTimeoutProp", m_useReadTimeoutProp.m_value);
	storage.readInt("readTimeoutProp", m_readTimeoutProp.m_value);
}

void UsbDataEndpointSession.updateProperties()
{
	m_deviceProp.m_currentIndex = m_deviceCombo.m_currentIndex;
	m_interfaceProp.m_currentIndex = m_interfaceCombo.m_currentIndex;
	m_outEndpointProp.m_currentIndex = m_outEndpointCombo.m_currentIndex;
	m_inEndpointProp.m_currentIndex = m_inEndpointCombo.m_currentIndex;
}

bool errorcode UsbDataEndpointSession.applyProperties()
{
	m_deviceCombo.m_currentIndex = m_deviceProp.m_currentIndex;
	m_interfaceCombo.m_currentIndex = m_interfaceProp.m_currentIndex;
	m_outEndpointCombo.m_currentIndex = m_outEndpointProp.m_currentIndex;
	m_inEndpointCombo.m_currentIndex = m_inEndpointProp.m_currentIndex;
	m_readBuffer.setSize(m_bufferPropertySet.m_readBufferSizeProp.m_value);

	if (m_inEndpoint)
		configureInEndpoint();

	if (m_outEndpoint)
		m_outEndpoint.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;

	return true;
}

void UsbDataEndpointSession.restoreDefaultProperties()
{
	m_autoDetachKernelDriverProp.m_value = Defaults.AutoDetachKernelDriver;
	m_useReadTimeoutProp.m_value = Defaults.UseReadTimeout;
	m_readTimeoutProp.m_value = Defaults.ReadTimeout;
	m_bufferPropertySet.restoreDefaults();
}

size_t errorcode UsbDataEndpointSession.transmit(
	void const* p,
	size_t size
	)
{
	if (m_state < State.Claimed || m_outEndpointCombo.m_currentIndex == -1)
		return -1;

	io.UsbEndpointDesc const* endpointDesc = m_outEndpointCombo.m_currentData;
	if (!m_outEndpoint || m_outEndpoint.m_endpointDesc.m_endpointId != endpointDesc.m_endpointId)
	{
		m_outEndpoint = m_interface.openEndpoint(endpointDesc.m_endpointId);
		m_outEndpoint.m_writeBufferSize = m_bufferPropertySet.m_writeBufferSizeProp.m_value;
	}

	size = m_outEndpoint.write(p, size);
	m_logWriter.write(log.StdRecordCode.Tx, p, size);
	return size;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
	return -1;
}

void UsbDataEndpointSession.onOutEndpointEvent(
	uint_t syncId,
	io.UsbEndpointEvents triggeredEvents
	)
{
	if (syncId != m_outSyncId)
		return;

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

	if (triggeredEvents & io.UsbEndpointEvents.WriteBufferReady)
		m_isTransmitEnabled = true;

	waitOutEndpointEvents();
}

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

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

	if ((triggeredEvents & io.UsbEndpointEvents.ReadBufferFull) &&
		m_bufferPropertySet.m_notifyReadBufferFullProp.m_value)
		m_logWriter.write(log.StdRecordCode.RxBufferFull);

	if (triggeredEvents & io.UsbEndpointEvents.IncomingData)
	{
		size_t result = m_inEndpoint.read(m_readBuffer.m_p, m_readBuffer.m_size);
		if (result)
			m_logWriter.write(log.StdRecordCode.Rx, m_readBuffer.m_p, result);
	}

	waitInEndpointEvents();
}

reactor UsbDataEndpointSession.m_uiReactor
{
	m_title = $"USB $(m_deviceCombo.m_currentText)";

	bindable bool isOpened = m_state >= State.Opened;
	bindable bool isClaimed = m_state >= State.Claimed;
	bindable bool isReading = m_state == State.Reading;

	m_actionTable[ActionId.Open].m_text = isOpened ? "Close Device" : "Open Device";
	m_actionTable[ActionId.Open].m_icon = m_iconTable[isOpened];
	m_actionTable[ActionId.Claim].m_text = isClaimed ? "Release Interface" : "Claim Interface";
	m_actionTable[ActionId.Claim].m_icon = m_iconTable[isClaimed];
	m_actionTable[ActionId.Claim].m_isEnabled = isOpened;
	m_actionTable[ActionId.Read].m_text = isReading ? "Stop reading" : "Start reading";
	m_actionTable[ActionId.Read].m_icon = m_iconTable[isReading];
	m_actionTable[ActionId.Read].m_isEnabled = isClaimed;

	m_interfaceCombo.m_isEnabled = isOpened;
	m_interfaceProp.m_isEnabled = isOpened;
	m_outEndpointCombo.m_isEnabled = isClaimed;
	m_outEndpointProp.m_isEnabled = isClaimed;
	m_inEndpointCombo.m_isEnabled = isClaimed;
	m_inEndpointProp.m_isEnabled = isClaimed;
	m_readTimeoutProp.m_isEnabled = m_useReadTimeoutProp.m_value;
	m_bufferPropertySet.m_readParallelismProp.m_isEnabled = !m_useReadTimeoutProp.m_value;
	m_bufferPropertySet.m_readBlockSizeProp.m_isEnabled = m_useReadTimeoutProp.m_value;

	m_isTransmitEnabled = isClaimed && m_outEndpointCombo.m_currentIndex != -1;

	m_statusPaneTable[StatusPaneId.State].m_text =
		isReading ? "Reading" :
		isClaimed ? "Claimed" :
		isOpened ? "Opened" : "Closed";
}

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