/* +------------------------------------------------------------------------+
   |                     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          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/config.h>
#include <mrpt/containers/circular_buffer.h>
#include <mrpt/io/CStream.h>

#include <deque>

namespace mrpt::comms
{
/** A list of FTDI devices and their descriptors.
 * \sa CInterfaceFTDI::ListAllDevices
 * \ingroup mrpt_comms_grp
 */
struct TFTDIDevice
{
	std::string ftdi_manufacturer;
	std::string ftdi_description;
	std::string ftdi_serial;

	uint16_t usb_idVendor;
	uint16_t usb_idProduct;
	uint8_t usb_serialNumber;

#if defined(MRPT_OS_LINUX) || defined(__APPLE__)
	/** Only for Linux: the corresponding libusb's `libusb_device*` (or
	 * `usb_device*` for libftdi <1.2) */
	void* usb_device_struct;
#endif
};

/** Print out all the information of a FTDI device in textual form. */
std::ostream& operator<<(std::ostream& o, const TFTDIDevice& d);

/** Used in  CInterfaceFTDI::ListAllDevices */
using TFTDIDeviceList = std::deque<TFTDIDevice>;

/** A definition of a CStream actually representing a USB connection to a FTDI
 *chip.
 *
 *  This class implements the communication with FT245BM / FT245RL chips.
 *   Using this class makes a program to depend on:
 *		- Windows: "FT2XX.DLL" and the device drivers (see FTDI website).
 *		- Linux: "libusb.so" (quite standard!), and "libftdi.so" only if linking
 *against the dynamic library.
 *
 *  If there is any error during the communications (or loading the Windows
 *DLL), a std::exception will be raised.
 *
 *  To write bulk data, use CStream::ReadBuffer and CStream::WriteBuffer.
 *
 * Warning: Avoid defining an object of this class in a global scope if you want
 *to catch all potential
 *      exceptions during the constructors (like DLL not found, etc...)
 *
 * VERSIONS:
 *		- 11/APR/2005: Initial development. JLBC
 *		- 16/FEB/2007: Integration into the MRPT framework. Support for device
 *serial numbers. JLBC
 *		- 15/APR/2008: Implemented for Linux using libftdi. JLBC
 *
 * \sa CStream
 * \ingroup mrpt_comms_grp
 */
class CInterfaceFTDI : public mrpt::io::CStream
{
   public:
	/** Constructor, which loads driver interface (the DLL under Windows).
	 */
	CInterfaceFTDI();

	/** Destructor, which closes the connection with the chip and unloads the
	 * driver interface.
	 */
	~CInterfaceFTDI() override;

	/** This object cannot be copied */
	CInterfaceFTDI(const CInterfaceFTDI& o) = delete;

	/** This object cannot be copied */
	CInterfaceFTDI& operator=(const CInterfaceFTDI& o) = delete;

	/** Checks whether the chip has been successfully open.
	 * \sa OpenBySerialNumber, OpenByDescription
	 */
	bool isOpen();

	/** Open by device serial number
	 */
	void OpenBySerialNumber(const std::string& serialNumber);

	/** Open by device description
	 */
	void OpenByDescription(const std::string& description);

	/** Close the USB device */
	void Close();

	/** Reset the USB device */
	void ResetDevice();

	/** Purge the I/O buffers */
	void Purge();

	/** Change the latency timer (in milliseconds) implemented on the FTDI chip:
	 * for a few ms, data is not sent to the PC waiting for possible more data,
	 * to save USB trafic. */
	void SetLatencyTimer(unsigned char latency_ms);

	/** Change read & write timeouts, in milliseconds. */
	void SetTimeouts(
		unsigned long dwReadTimeout_ms, unsigned long dwWriteTimeout_ms);

	/** Generates a list with all FTDI devices connected right now.
	 */
	void ListAllDevices(TFTDIDeviceList& outList);

	/** Tries to read, raising no exception if not all the bytes are available,
	 * but raising one if there is some communication error.
	 */
	size_t ReadSync(void* Buffer, size_t Count) { return Read(Buffer, Count); }
	/** Tries to write, raising no exception if not all the bytes are available,
	 * but raising one if there is some communication error.
	 */
	size_t WriteSync(const void* Buffer, size_t Count)
	{
		return Write(Buffer, Count);
	}

	/** Reads a block of bytes from the stream into Buffer, and returns the
	 *amound of bytes actually read, without waiting for more extra bytes to
	 *arrive (just those already enqued in the stream).
	 *  In this class this method actually behaves as expected and does not
	 *fallback to ReadBuffer().
	 *	\exception std::exception On any error, or if ZERO bytes are read.
	 */
	size_t ReadBufferImmediate(void* Buffer, size_t Count) override;

	/** Introduces a pure virtual method responsible for reading from the
	 * stream.
	 *  It integrates a cache buffer to speed-up sequences of many, small
	 * readings.
	 */
	size_t Read(void* Buffer, size_t Count) override;

	/** Introduces a pure virtual method responsible for writing to the stream.
	 *  Write attempts to write up to Count bytes to Buffer, and returns the
	 * number of bytes actually written.
	 */
	size_t Write(const void* Buffer, size_t Count) override;

	/** This virtual method does nothing in this class.
	 */
	uint64_t Seek(
		int64_t Offset, CStream::TSeekOrigin Origin = sFromBeginning) override;

	/** This virtual method does nothing in this class.
	 */
	uint64_t getTotalBytesCount() const override;

	/** This virtual method does nothing in this class.
	 */
	uint64_t getPosition() const override;

   protected:
	/** Used in Read */
	mrpt::containers::circular_buffer<uint8_t> m_readBuffer;

	void ftdi_read(
		void* lpvBuffer, unsigned long dwBuffSize,
		unsigned long* lpdwBytesRead);
	void ftdi_write(
		const void* lpvBuffer, unsigned long dwBuffSize,
		unsigned long* lpdwBytes);

#if defined(_WIN32)
   private:
	void checkErrorAndRaise(int errorCode);

	void ftdi_open(void* pvDevice);
	void ftdi_openEx(void* pArg1, unsigned long dwFlags);
	void ftdi_listDevices(void* pArg1, void* pArg2, unsigned long dwFlags);
	void ftdi_getQueueStatus(unsigned long* lpdwAmountInRxQueue);

	void* m_hmodule;
	unsigned long m_ftHandle;

	void loadDriver();

	enum FT_STATUS{dummy};

	typedef FT_STATUS(__stdcall* PtrToOpen)(void*, unsigned long*);
	PtrToOpen m_pOpen;

	typedef FT_STATUS(__stdcall* PtrToOpenEx)(
		void*, unsigned long, unsigned long*);
	PtrToOpenEx m_pOpenEx;

	typedef FT_STATUS(__stdcall* PtrToListDevices)(void*, void*, unsigned long);
	PtrToListDevices m_pListDevices;

	typedef FT_STATUS(__stdcall* PtrToClose)(unsigned long);
	PtrToClose m_pClose;

	typedef FT_STATUS(__stdcall* PtrToRead)(
		unsigned long, void*, unsigned long, unsigned long*);
	PtrToRead m_pRead;

	typedef FT_STATUS(__stdcall* PtrToWrite)(
		unsigned long, const void*, unsigned long, unsigned long*);
	PtrToWrite m_pWrite;

	typedef FT_STATUS(__stdcall* PtrToResetDevice)(unsigned long);
	PtrToResetDevice m_pResetDevice;

	typedef FT_STATUS(__stdcall* PtrToPurge)(unsigned long, unsigned long);
	PtrToPurge m_pPurge;

	typedef FT_STATUS(__stdcall* PtrToSetTimeouts)(
		unsigned long, unsigned long, unsigned long);
	PtrToSetTimeouts m_pSetTimeouts;

	typedef FT_STATUS(__stdcall* PtrToGetQueueStatus)(
		unsigned long, unsigned long*);
	PtrToGetQueueStatus m_pGetQueueStatus;

	typedef FT_STATUS(__stdcall* PtrToSetLatencyTimer)(
		unsigned long, unsigned char);
	PtrToSetLatencyTimer m_pSetLatencyTimer;

#else
	// Declarations for Linux:
	void* m_ftdi_context;

	/** Process recursively a USB device and its children: */
	void recursive_fill_list_devices(
		void* usb_device_structure, TFTDIDeviceList& outList);

#endif

};	// end of class

}  // namespace mrpt::comms
