#include "nsyshid.h"

#include <libusb-1.0/libusb.h>
#include "Backend.h"

namespace nsyshid::backend::libusb
{
	enum : uint8
	{
		HID_CLASS_GET_REPORT   = 0x01,
		HID_CLASS_GET_IDLE     = 0x02,
		HID_CLASS_GET_PROTOCOL = 0x03,
		HID_CLASS_SET_REPORT   = 0x09,
		HID_CLASS_SET_IDLE     = 0x0A,
		HID_CLASS_SET_PROTOCOL = 0x0B
	};

	class BackendLibusb : public nsyshid::Backend {
	  public:
		BackendLibusb();

		~BackendLibusb();

		bool IsInitialisedOk() override;

	  protected:
		void AttachVisibleDevices() override;

	  private:
		libusb_context* m_ctx;
		int m_initReturnCode;
		bool m_callbackRegistered;
		libusb_hotplug_callback_handle m_hotplugCallbackHandle;
		std::thread m_hotplugThread;
		std::atomic<bool> m_hotplugThreadStop;

		// called by libusb
		static int HotplugCallback(libusb_context* ctx, libusb_device* dev,
								   libusb_hotplug_event event, void* user_data);

		int OnHotplug(libusb_device* dev, libusb_hotplug_event event);

		std::shared_ptr<Device> CheckAndCreateDevice(libusb_device* dev);

		std::shared_ptr<Device> FindLibusbDevice(libusb_device* dev);

		bool FindDefaultDeviceEndpoints(libusb_device* dev,
										bool& endpointInFound, uint8& endpointIn, uint16& endpointInMaxPacketSize,
										bool& endpointOutFound, uint8& endpointOut, uint16& endpointOutMaxPacketSize);
	};

	template<typename T>
	using UniquePtr = std::unique_ptr<T, void (*)(T*)>;

	using ConfigDescriptor = UniquePtr<libusb_config_descriptor>;

	class DeviceLibusb : public nsyshid::Device {
	  public:
		DeviceLibusb(libusb_context* ctx,
					 uint16 vendorId,
					 uint16 productId,
					 uint8 interfaceIndex,
					 uint8 interfaceSubClass,
					 uint8 protocol,
					 uint8 libusbBusNumber,
					 uint8 libusbDeviceAddress,
					 std::vector<ConfigDescriptor> configs);

		~DeviceLibusb() override;

		bool Open() override;

		void Close() override;

		bool IsOpened() override;

		ReadResult Read(ReadMessage* message) override;

		WriteResult Write(WriteMessage* message) override;

		bool GetDescriptor(uint8 descType,
						   uint8 descIndex,
						   uint16 lang,
						   uint8* output,
						   uint32 outputMaxLength) override;

		bool SetIdle(uint8 ifIndex,
					 uint8 reportId,
					 uint8 duration) override;

		bool SetProtocol(uint8 ifIndex, uint8 protocol) override;

		int ClaimAllInterfaces(uint8 config_num);
		int ReleaseAllInterfaces(uint8 config_num);
		int ReleaseAllInterfacesForCurrentConfig();

		bool SetReport(ReportMessage* message) override;

		uint8 m_libusbBusNumber;
		uint8 m_libusbDeviceAddress;
		bool m_libusbHasEndpointIn;
		uint8 m_libusbEndpointIn;
		bool m_libusbHasEndpointOut;
		uint8 m_libusbEndpointOut;

	  private:
		void CloseDevice();

		libusb_context* m_ctx;
		std::mutex m_handleMutex;
		std::atomic<sint32> m_handleInUseCounter;
		std::condition_variable m_handleInUseCounterDecremented;
		libusb_device_handle* m_libusbHandle;
		std::vector<ConfigDescriptor> m_config_descriptors;

		class HandleLock {
		  public:
			HandleLock() = delete;

			HandleLock(libusb_device_handle** handle,
					   std::mutex& handleMutex,
					   std::atomic<sint32>& handleInUseCounter,
					   std::condition_variable& handleInUseCounterDecremented,
					   DeviceLibusb& device);

			~HandleLock();

			HandleLock(const HandleLock&) = delete;

			HandleLock& operator=(const HandleLock&) = delete;

			bool IsValid();

			libusb_device_handle* GetHandle();

		  private:
			libusb_device_handle* m_handle;
			std::mutex& m_handleMutex;
			std::atomic<sint32>& m_handleInUseCounter;
			std::condition_variable& m_handleInUseCounterDecremented;
		};

		std::unique_ptr<HandleLock> AquireHandleLock();
	};
} // namespace nsyshid::backend::libusb
