/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "comms-precomp.h"	// Precompiled headers
//
#include <iostream>

#ifdef _WIN32

/*===========================================================================
	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
								START OF FTD2XX.H
	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ===========================================================================*/
namespace comms
{
// The following ifdef block is the standard way of creating macros which make
// exporting
// from a DLL simpler. All files within this DLL are compiled with the
// FTD2XX_EXPORTS
// symbol defined on the command line. this symbol should not be defined on any
// project
// that uses this DLL. This way any other project whose source files include
// this file see
// FTD2XX_API functions as being imported from a DLL, wheras this DLL sees
// symbols
// defined with this macro as being exported.
#ifdef FTD2XX_EXPORTS
#define FTD2XX_API __declspec(dllexport)
#else
#define FTD2XX_API __declspec(dllimport)
#endif

using FT_HANDLE = unsigned long;

//
// FT_OpenEx Flags
//

#define FT_OPEN_BY_SERIAL_NUMBER 1
#define FT_OPEN_BY_DESCRIPTION 2

//
// FT_ListDevices Flags (used in conjunction with FT_OpenEx Flags
//

#define FT_LIST_NUMBER_ONLY 0x80000000
#define FT_LIST_BY_INDEX 0x40000000
#define FT_LIST_ALL 0x20000000

#define FT_LIST_MASK (FT_LIST_NUMBER_ONLY | FT_LIST_BY_INDEX | FT_LIST_ALL)

//
// Baud Rates
//

#define FT_BAUD_300 300
#define FT_BAUD_600 600
#define FT_BAUD_1200 1200
#define FT_BAUD_2400 2400
#define FT_BAUD_4800 4800
#define FT_BAUD_9600 9600
#define FT_BAUD_14400 14400
#define FT_BAUD_19200 19200
#define FT_BAUD_38400 38400
#define FT_BAUD_57600 57600
#define FT_BAUD_115200 115200
#define FT_BAUD_230400 230400
#define FT_BAUD_460800 460800
#define FT_BAUD_921600 921600

//
// Word Lengths
//

#define FT_BITS_8 (unsigned char)8
#define FT_BITS_7 (unsigned char)7
#define FT_BITS_6 (unsigned char)6
#define FT_BITS_5 (unsigned char)5

//
// Stop Bits
//

#define FT_STOP_BITS_1 (unsigned char)0
#define FT_STOP_BITS_1_5 (unsigned char)1
#define FT_STOP_BITS_2 (unsigned char)2

//
// Parity
//

#define FT_PARITY_NONE (unsigned char)0
#define FT_PARITY_ODD (unsigned char)1
#define FT_PARITY_EVEN (unsigned char)2
#define FT_PARITY_MARK (unsigned char)3
#define FT_PARITY_SPACE (unsigned char)4

//
// Flow Control
//

#define FT_FLOW_NONE 0x0000
#define FT_FLOW_RTS_CTS 0x0100
#define FT_FLOW_DTR_DSR 0x0200
#define FT_FLOW_XON_XOFF 0x0400

//
// Purge rx and tx buffers
//
#define FT_PURGE_RX 1
#define FT_PURGE_TX 2

//
// Events
//

using PFT_EVENT_HANDLER = void (*)(unsigned long, unsigned, long);

#define FT_EVENT_RXCHAR 1
#define FT_EVENT_MODEM_STATUS 2

//
// Timeouts
//

#define FT_DEFAULT_RX_TIMEOUT 300
#define FT_DEFAULT_TX_TIMEOUT 300

}  // end namespace comms
/*===========================================================================
	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
								END OF FTD2XX.H
	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ===========================================================================*/

#include <mrpt/comms/CInterfaceFTDI.h>
#include <mrpt/core/exceptions.h>

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

using namespace mrpt;
using namespace mrpt::comms;
using namespace std;

/*-------------------------------------------------------------
					CInterfaceFTDI
-------------------------------------------------------------*/
CInterfaceFTDI::CInterfaceFTDI() : m_readBuffer(4096)
{
	MRPT_START

	m_ftHandle = 0;
	loadDriver();

	MRPT_END
}

/*-------------------------------------------------------------
					~CInterfaceFTDI
-------------------------------------------------------------*/
CInterfaceFTDI::~CInterfaceFTDI()
{
	if (m_hmodule != nullptr)
	{
		// Close USB connection:
		this->Close();

		// Unload FT2XX DLL:
		FreeLibrary((HMODULE)m_hmodule);
		m_hmodule = nullptr;
	}
}

bool CInterfaceFTDI::isOpen() { return m_ftHandle != 0; }
void CInterfaceFTDI::loadDriver()
{
	MRPT_START
	// ------------------------------------------------------
	//				Windoze version
	// ------------------------------------------------------
	m_hmodule = ::LoadLibraryA("Ftd2xx.dll");
	if (m_hmodule == nullptr) THROW_EXCEPTION("Error: Cannot load Ftd2xx.dll");

	m_pWrite = (PtrToWrite)GetProcAddress((HMODULE)m_hmodule, "FT_Write");
	m_pRead = (PtrToRead)GetProcAddress((HMODULE)m_hmodule, "FT_Read");
	m_pOpen = (PtrToOpen)GetProcAddress((HMODULE)m_hmodule, "FT_Open");
	m_pOpenEx = (PtrToOpenEx)GetProcAddress((HMODULE)m_hmodule, "FT_OpenEx");
	m_pListDevices =
		(PtrToListDevices)GetProcAddress((HMODULE)m_hmodule, "FT_ListDevices");
	m_pClose = (PtrToClose)GetProcAddress((HMODULE)m_hmodule, "FT_Close");
	m_pResetDevice =
		(PtrToResetDevice)GetProcAddress((HMODULE)m_hmodule, "FT_ResetDevice");
	m_pPurge = (PtrToPurge)GetProcAddress((HMODULE)m_hmodule, "FT_Purge");
	m_pSetTimeouts =
		(PtrToSetTimeouts)GetProcAddress((HMODULE)m_hmodule, "FT_SetTimeouts");
	m_pGetQueueStatus = (PtrToGetQueueStatus)GetProcAddress(
		(HMODULE)m_hmodule, "FT_GetQueueStatus");
	m_pSetLatencyTimer = (PtrToSetLatencyTimer)GetProcAddress(
		(HMODULE)m_hmodule, "FT_SetLatencyTimer");

	if (!m_pWrite || !m_pRead || !m_pOpen || !m_pOpenEx || !m_pListDevices ||
		!m_pClose || !m_pResetDevice || !m_pPurge || !m_pSetTimeouts ||
		!m_pGetQueueStatus || !m_pSetLatencyTimer)
		THROW_EXCEPTION("Error loading FTD2XX.DLL");

	MRPT_END
}

/*-------------------------------------------------------------
				FTD2XX.DLL INTERFACE FUNCTIONS
-------------------------------------------------------------*/
void CInterfaceFTDI::ftdi_open(void* pvDevice)
{
	MRPT_START
	if (isOpen()) Close();

	ASSERT_(m_pOpen);
	checkErrorAndRaise((*m_pOpen)(pvDevice, &m_ftHandle));

	MRPT_END
}

void CInterfaceFTDI::ftdi_openEx(void* pArg1, unsigned long dwFlags)
{
	MRPT_START
	if (isOpen()) Close();

	ASSERT_(m_pOpenEx);
	checkErrorAndRaise((*m_pOpenEx)(pArg1, dwFlags, &m_ftHandle));

	MRPT_END
}

/*-------------------------------------------------------------
					ListAllDevices
-------------------------------------------------------------*/
void CInterfaceFTDI::ListAllDevices(TFTDIDeviceList& outList)
{
	MRPT_START

	outList.clear();

	unsigned long nConectedDevices;
	char str[100];

	// Get the number of devices:
	ftdi_listDevices(&nConectedDevices, nullptr, 0x80000000);

	for (size_t i = 0; i < nConectedDevices; i++)
	{
		TFTDIDevice newEntry;

		// Serial number:
		ftdi_listDevices(
			(void*)(i), (void*)str, (unsigned long)(0x40000000 | 1));
		newEntry.ftdi_serial = str;

		// Description:
		ftdi_listDevices(
			(void*)(i), (void*)str, (unsigned long)(0x40000000 | 2));
		newEntry.ftdi_description = str;

		outList.push_back(newEntry);
	}

	MRPT_END
}

void CInterfaceFTDI::ftdi_listDevices(
	void* pArg1, void* pArg2, unsigned long dwFlags)
{
	MRPT_START

	ASSERT_(m_pListDevices);
	checkErrorAndRaise((*m_pListDevices)(pArg1, pArg2, dwFlags));

	MRPT_END
}

void CInterfaceFTDI::Close()
{
	MRPT_START

	if (m_ftHandle)
	{
		ASSERT_(m_pClose);
		(*m_pClose)(m_ftHandle);
		m_ftHandle = 0;
	}

	m_readBuffer.clear();

	MRPT_END
}

void CInterfaceFTDI::ftdi_read(
	void* lpvBuffer, unsigned long dwBuffSize, unsigned long* lpdwBytesRead)
{
	MRPT_START

	ASSERT_(m_pRead);
	checkErrorAndRaise(
		(*m_pRead)(m_ftHandle, lpvBuffer, dwBuffSize, lpdwBytesRead));

	MRPT_END
}

void CInterfaceFTDI::ftdi_write(
	const void* lpvBuffer, unsigned long dwBuffSize, unsigned long* lpdwBytes)
{
	MRPT_START

	ASSERT_(m_pWrite);
	checkErrorAndRaise(
		(*m_pWrite)(m_ftHandle, lpvBuffer, dwBuffSize, lpdwBytes));

	MRPT_END
}

void CInterfaceFTDI::ResetDevice()
{
	MRPT_START

	ASSERT_(m_pResetDevice);
	checkErrorAndRaise((*m_pResetDevice)(m_ftHandle));

	m_readBuffer.clear();

	MRPT_END
}

void CInterfaceFTDI::Purge()
{
	MRPT_START

	ASSERT_(m_pPurge);
	unsigned long dwMask = FT_PURGE_RX | FT_PURGE_TX;
	checkErrorAndRaise((*m_pPurge)(m_ftHandle, dwMask));

	m_readBuffer.clear();
	MRPT_END
}

void CInterfaceFTDI::SetTimeouts(
	unsigned long dwReadTimeout_ms, unsigned long dwWriteTimeout_ms)
{
	MRPT_START

	ASSERT_(m_pSetTimeouts);
	checkErrorAndRaise(
		(*m_pSetTimeouts)(m_ftHandle, dwReadTimeout_ms, dwWriteTimeout_ms));

	MRPT_END
}

void CInterfaceFTDI::ftdi_getQueueStatus(unsigned long* lpdwAmountInRxQueue)
{
	MRPT_START

	ASSERT_(m_pGetQueueStatus);
	checkErrorAndRaise((*m_pGetQueueStatus)(m_ftHandle, lpdwAmountInRxQueue));

	MRPT_END
}

void CInterfaceFTDI::SetLatencyTimer(unsigned char latency_ms)
{
	MRPT_START

	ASSERT_(m_pSetLatencyTimer);
	checkErrorAndRaise((*m_pSetLatencyTimer)(m_ftHandle, latency_ms));

	MRPT_END
}

/*-------------------------------------------------------------
					checkErrorAndRaise
-------------------------------------------------------------*/
void CInterfaceFTDI::checkErrorAndRaise(int errorCode)
{
	/** Possible responses from the driver
	enum FT_STATUS
	{
		FT_OK = 0,
		FT_INVALID_HANDLE,
		FT_DEVICE_NOT_FOUND,
		FT_DEVICE_NOT_OPENED,
		FT_IO_ERROR,
		FT_INSUFFICIENT_RESOURCES,
		FT_INVALID_PARAMETER
	};  */
	switch (errorCode)
	{
		case 0: return;
		case 1:
			Close();
			THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_INVALID_HANDLE");
		case 2:
			Close();
			THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_DEVICE_NOT_FOUND");
		case 3:
			Close();
			THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_DEVICE_NOT_OPENED");
		case 4: Close(); THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_IO_ERROR");
		case 5:
			THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_INSUFFICIENT_RESOURCES");
		case 6: THROW_EXCEPTION("*** FTD2XX ERROR ***: FT_INVALID_PARAMETER");
		default:
			THROW_EXCEPTION("*** FTD2XX ERROR ***: Invalid error code!?!?!?");
	};
}

/*-------------------------------------------------------------
					OpenBySerialNumber
-------------------------------------------------------------*/
void CInterfaceFTDI::OpenBySerialNumber(const std::string& serialNumber)
{
	MRPT_START
	m_readBuffer.clear();

	ftdi_openEx((void*)serialNumber.c_str(), FT_OPEN_BY_SERIAL_NUMBER);
	MRPT_END
}

/*-------------------------------------------------------------
					OpenByDescription
-------------------------------------------------------------*/
void CInterfaceFTDI::OpenByDescription(const std::string& description)
{
	MRPT_START
	m_readBuffer.clear();

	ftdi_openEx((void*)description.c_str(), FT_OPEN_BY_DESCRIPTION);
	MRPT_END
}

/*-------------------------------------------------------------
					OpenByDescription
-------------------------------------------------------------*/
std::ostream& mrpt::comms::operator<<(std::ostream& o, const TFTDIDevice& d)
{
	o << "Manufacturer            : " << d.ftdi_manufacturer << endl
	  << "Description             : " << d.ftdi_description << endl
	  << "FTDI serial             : " << d.ftdi_serial << endl
	  << "USB ID (Vendor/Product) : "
	  << format("%04X / %04X", d.usb_idVendor, d.usb_idProduct) << endl
	  << "USB serial              : " << d.usb_serialNumber << endl;

	return o;
}

#endif
