#pragma once

#include "../../core/utils.hpp"
#include "../support/thread_extra.hpp"
#include "camera_model.hpp"

// Workaround for https://github.com/chriskohlhoff/asio/issues/1441
// libusb also includes winsock.h on Windows which conflicts with ASIO, so we prevent that
// by including winsock2.h first, which is the newer API ASIO uses.
#if defined(_WIN32)
#	if defined(WIN32_LEAN_AND_MEAN)
#		include <winsock2.h>
#		include <libusb.h>
#	else
#		define WIN32_LEAN_AND_MEAN
#		include <winsock2.h>
#		include <libusb.h>
#		undef WIN32_LEAN_AND_MEAN
#	endif
#else
#	include <libusb.h>
#endif

#include <boost/endian.hpp>

#include <atomic>
#include <shared_mutex>
#include <thread>
#include <unordered_map>

#define DV_LIBUSB_VERSION_1_0_23    0x01000107
#define DV_LIBUSB_VERSION_1_0_24    0x01000108
#define DV_LIBUSB_VERSION_1_0_25_26 0x01000109
#define DV_LIBUSB_VERSION_1_0_27_28 0x0100010A
#define DV_LIBUSB_VERSION_1_0_29    0x0100010B

namespace dv::io::camera {
class USBDevice {
public:
	// Debug logging severity levels.
	enum class LogLevel : uint8_t {
		LVL_NONE    = 0,
		LVL_ERROR   = 1,
		LVL_WARNING = 2,
		LVL_INFO    = 3,
		LVL_DEBUG   = 4,
	};

	friend std::ostream &operator<<(std::ostream &os, const LogLevel &var) {
		switch (var) {
			case LogLevel::LVL_ERROR:
				return os << "ERROR";

			case LogLevel::LVL_WARNING:
				return os << "WARNING";

			case LogLevel::LVL_INFO:
				return os << "INFO";

			case LogLevel::LVL_DEBUG:
				return os << "DEBUG";
		}

		return os;
	}

	/**
	 * Structure to uniquely describe a USB device connected to the current host.
	 */
	struct DeviceDescriptor {
		std::string serialNumber;
		uint16_t vid;
		uint16_t pid;
		USBDeviceType deviceType;
		CameraModel cameraModel;
		uint8_t busNumber;
		uint8_t devAddress;
		uint8_t firmwareVersion;
	};

	using loggerCallbackType
		= std::function<void(LogLevel level, std::string_view deviceName, std::string_view message)>;

	/**
	 * Get device descriptor. Describes this device uniquely.
	 * @return this device's descriptor structure
	 */
	[[nodiscard]] DeviceDescriptor getDeviceDescriptor() const {
		return mDescriptor;
	}

	/**
	 * Get camera firmware version.
	 * @return camera firmware version
	 */
	[[nodiscard]] uint8_t getFirmwareVersion() const {
		return mDescriptor.firmwareVersion;
	}

	/**
	 * Get camera serial number.
	 * @return camera serial number
	 */
	[[nodiscard]] std::string getSerialNumber() const {
		return mDescriptor.serialNumber;
	}

	/**
	 * Get camera name.
	 * @return camera name
	 */
	[[nodiscard]] std::string getDeviceName() const {
		std::shared_lock lock{mUsbDeviceNameLock};

		return mUsbDeviceName;
	}

	/**
	 * Get USB connection speed.
	 * @return USB connection speed
	 */
	[[nodiscard]] libusb_speed getConnectionSpeed() const {
		return dv::IntegerAsEnum<libusb_speed>(libusb_get_device_speed(libusb_get_device(mDeviceHandle.get())));
	}

	/**
	 * Get unique USB port path, consisting of bus number and port numbers.
	 * @return USB unique port path
	 */
	[[nodiscard]] std::string getPortPath() const {
		uint8_t buffer[64]{};
		const auto bufferSize = libusb_get_port_numbers(libusb_get_device(mDeviceHandle.get()), buffer, sizeof(buffer));

		std::string portPath{};
		portPath.append(fmt::format("{}:", mDescriptor.busNumber));

		for (size_t i = 0; i < bufferSize; i++) {
			portPath.append(fmt::format("{}.", buffer[i]));
		}

		portPath.pop_back();

		return portPath;
	}

	/**
	 * Set device's log-level.
	 * @param level device log-level
	 */
	void setLogLevel(const LogLevel level) {
		// Set USB log-level to this value too.
		mUsbLogLevel = level;

		{
			// libusb debug generates too much noise.
			auto logLevelLibusb = level;
			if (logLevelLibusb == LogLevel::LVL_DEBUG) {
				logLevelLibusb = LogLevel::LVL_INFO;
			}

			libusb_set_option(
				mDeviceContext.get(), LIBUSB_OPTION_LOG_LEVEL, static_cast<libusb_log_level>(logLevelLibusb));
		}

		// Same log-level on the device.
		try {
			usbControlTransferOut(VENDOR_REQUEST_LOG_LEVEL, dv::EnumAsInteger(level), 0, {});
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Unsupported on old devices.
		}
	}

	/**
	 * Get device's log-level.
	 * @return device log-level
	 */
	[[nodiscard]] LogLevel getLogLevel() const {
		return mUsbLogLevel;
	}

	/**
	 * Set number of USB buffers used for data transfer.
	 * @param transfersNumber number of USB buffers used for data transfer.
	 */
	void setTransfersNumber(const uint32_t transfersNumber) {
		std::scoped_lock lock{mUsbOpsLock, mDataTransfersLock};

		// Value already set, don't do expensive operation.
		if (mDataTransfersNumberNLCK == transfersNumber) {
			return;
		}

		mDataTransfersNumberNLCK = transfersNumber;

		// Cancel transfers, wait for them to terminate, deallocate, and
		// then reallocate with new size/number.
		if (isUSBDataTransferRunning()) {
			usbCancelAndDeallocateDataTransfersNLCK();
			usbAllocateDataTransfersNLCK();
		}
	}

	/**
	 * Get number of USB buffers used for data transfer.
	 * @return number of USB buffers used for data transfer.
	 */
	[[nodiscard]] uint32_t getTransfersNumber() const {
		std::scoped_lock lock{mDataTransfersLock};

		return mDataTransfersNumberNLCK;
	}

	/**
	 * Set size in bytes of USB buffers used for data transfer.
	 * @param transfersSize size in bytes of USB buffers used for data transfer.
	 */
	void setTransfersSize(const uint32_t transfersSize) {
		std::scoped_lock lock{mUsbOpsLock, mDataTransfersLock};

		// Value already set, don't do expensive operation.
		if (mDataTransfersSizeNLCK == transfersSize) {
			return;
		}

		mDataTransfersSizeNLCK = transfersSize;

		// Cancel transfers, wait for them to terminate, deallocate, and
		// then reallocate with new size/number.
		if (isUSBDataTransferRunning()) {
			usbCancelAndDeallocateDataTransfersNLCK();
			usbAllocateDataTransfersNLCK();
		}
	}

	/**
	 * Get size in bytes of USB buffers used for data transfer.
	 * @return size in bytes of USB buffers used for data transfer.
	 */
	[[nodiscard]] uint32_t getTransfersSize() const {
		std::scoped_lock lock{mDataTransfersLock};

		return mDataTransfersSizeNLCK;
	}

protected:
	static constexpr uint8_t VENDOR_REQUEST_SPI_CONFIG{0xBF};
	static constexpr uint16_t VID_INIVATION{0x152A};

	using compatibleCameraCallbackType
		= std::function<std::optional<CameraModel>(uint16_t vid, uint16_t pid, USBDeviceType deviceType)>;
	using controlOutCallbackType  = std::function<void(libusb_transfer_status status)>;
	using controlInCallbackType   = std::function<void(libusb_transfer_status status, std::span<const uint8_t> buffer)>;
	using usbTransferCallbackType = std::function<void(std::span<const uint8_t> data)>;
	using usbShutdownCallbackType = std::function<void()>;

	// SPI config via USB implementation.
	PACKED_STRUCT(struct SPIConfigurationParameters {
		uint8_t moduleAddress;
		uint8_t parameterAddress;
		uint32_t parameterValue;
	});

	static_assert(offsetof(SPIConfigurationParameters, moduleAddress) == 0);
	static_assert(offsetof(SPIConfigurationParameters, parameterAddress) == 1);
	static_assert(offsetof(SPIConfigurationParameters, parameterValue) == 2);
	static_assert(sizeof(SPIConfigurationParameters) == 6);

	[[nodiscard]] static std::vector<DeviceDescriptor> findCompatibleDevices(
		const compatibleCameraCallbackType &compatibleDevice, loggerCallbackType logger) {
		if (!compatibleDevice) {
			throw dv::exceptions::RuntimeError("Compatible camera callback must be defined.");
		}

		LibusbManagedContext discoveryContext{nullptr, libusbContextDeleter};

		// libusb may create its own threads at this stage, so we temporarily set
		// a different thread name.
		{
			dv::io::support::ThreadNameSwitch temporaryThreadNameSwitch{"usb_discovery"};

			int status;
			libusb_context *discoveryContextCAPI{nullptr};

			// Fixed log-level of WARNING for discovery operations.
#if (LIBUSB_API_VERSION >= DV_LIBUSB_VERSION_1_0_27_28)
			std::array<libusb_init_option, 2> initOptions{};
			initOptions[0].option          = LIBUSB_OPTION_LOG_LEVEL;
			initOptions[0].value.ival      = LIBUSB_LOG_LEVEL_WARNING;
			initOptions[1].option          = LIBUSB_OPTION_LOG_CB;
			initOptions[1].value.log_cbval = libusbLogCallback;

			status = libusb_init_context(&discoveryContextCAPI, initOptions.data(), initOptions.size());
#else
			status = libusb_init(&discoveryContextCAPI);

			if (status == LIBUSB_SUCCESS) {
				libusb_set_option(discoveryContextCAPI, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING);
				libusb_set_log_cb(discoveryContextCAPI, libusbLogCallback, LIBUSB_LOG_CB_CONTEXT);
			}
#endif

			if (status != LIBUSB_SUCCESS) {
				throw dv::exceptions::RuntimeError(errorPrint("Failed to initialize libusb context.", status));
			}

			discoveryContext.reset(discoveryContextCAPI);

			{
				std::unique_lock lock{LIBUSB_LOG_MAP_LOCK};
				LIBUSB_LOG_MAP[discoveryContextCAPI]
					= [logger = std::move(logger)](const LogLevel level, const std::string_view message) {
						  if (logger) {
							  logger(level, "usb_discovery", message);
						  }
					  };
			}
		}

		LibusbManagedDeviceList devicesList{nullptr, libusbDeviceListDeleter};
		size_t devicesListSize;

		{
			libusb_device **devicesListCAPI{nullptr};
			const auto result = libusb_get_device_list(discoveryContext.get(), &devicesListCAPI);
			if (result < 0) {
				throw dv::exceptions::RuntimeError(errorPrint("Failed to list USB devices.", static_cast<int>(result)));
			}

			devicesList.reset(devicesListCAPI);
			devicesListSize = static_cast<size_t>(result);
		}

		std::vector<DeviceDescriptor> foundDevices{};

		// Cycle thorough all discovered devices.
		for (size_t i = 0; i < devicesListSize; i++) {
			libusb_device_descriptor devDesc{};

			if (libusb_get_device_descriptor(devicesList[i], &devDesc) != LIBUSB_SUCCESS) {
				continue;
			}

			// Check if this is a compatible device (VID/PID).
			const auto deviceType = dv::IntegerAsEnum<USBDeviceType>((devDesc.bcdDevice >> 8) & 0xFF);

			const auto compat = compatibleDevice(devDesc.idVendor, devDesc.idProduct, deviceType);

			if (!compat) {
				continue;
			}

			DeviceDescriptor currInfo{};

			currInfo.vid         = devDesc.idVendor;
			currInfo.pid         = devDesc.idProduct;
			currInfo.deviceType  = deviceType;
			currInfo.cameraModel = compat.value();

			// Get USB bus number and device address from descriptors.
			currInfo.busNumber  = libusb_get_bus_number(devicesList[i]);
			currInfo.devAddress = libusb_get_device_address(devicesList[i]);

			// Get device firmware version before opening, so that firmwareVersion
			// is always defined. We only need the device descriptors for this.
			currInfo.firmwareVersion = (devDesc.bcdDevice & 0xFF);

			LibusbManagedDeviceHandle deviceHandle{nullptr, libusbDeviceHandleDeleter};

			{
				libusb_device_handle *deviceHandleCAPI{nullptr};

				if (const auto status = libusb_open(devicesList[i], &deviceHandleCAPI); status != LIBUSB_SUCCESS) {
					// Push without serial number.
					foundDevices.push_back(currInfo);
					continue;
				}

				deviceHandle.reset(deviceHandleCAPI);
			}

			if (devDesc.iSerialNumber == 0) {
				// Unknown serial number.
				currInfo.serialNumber = generateRepeatableSerialNumber(currInfo);
			}
			else {
				// Get the device's serial number.
				currInfo.serialNumber = fetchSerialNumber(deviceHandle.get(), devDesc.iSerialNumber).value_or("");
			}

			// Push with full information.
			foundDevices.push_back(currInfo);
		}

		return foundDevices;
	}

	[[nodiscard]] static std::vector<DeviceDescriptor> filterCompatibleDevices(
		const std::vector<DeviceDescriptor> &list, const std::string_view filterBySerialNumber) {
		std::vector<DeviceDescriptor> filtered{};

		std::copy_if(list.cbegin(), list.cend(), std::back_inserter(filtered),
			[filterBySerialNumber](const DeviceDescriptor &elem) {
				return (filterBySerialNumber.empty() || (elem.serialNumber == filterBySerialNumber));
			});

		return filtered;
	}

	/**
	 * Construct and open a USB device. Match is based on VID, PID, USB Bus Address. If present, serial number is also
	 * verified to match.
	 *
	 * @param deviceToOpen description of which device to open exactly. VID, PID, USB Bus Address must be defined.
	 * @param deviceLogger per-device logging callback. Any such logger *must* be thread-safe.
	 * @param deviceLogLevel initial per-device log-level for info logging, can be changed later with setLogLevel().
	 * @param shortName brief name for related device threads. Should be at most 6 characters for maximum compatibility.
	 * @param longName long name for full device name, used together with serial number in logging.
	 * @param expectedDevice decide if given descriptor is valid to select a compatible camera model on opening.
	 * @param requiredFirmwareVersion required firmware version for successfull operation of this device (-1 disables
	 * the check, else the exact version given must correspond to the device one).
	 */
	USBDevice(const DeviceDescriptor &deviceToOpen, loggerCallbackType deviceLogger, const LogLevel deviceLogLevel,
		const std::string_view shortName, const std::string_view longName,
		const compatibleCameraCallbackType &expectedDevice, const int32_t requiredFirmwareVersion = -1) :
		mUsbLogLevel(deviceLogLevel),
		mUsbDeviceLogger(std::move(deviceLogger)),
		mDescriptor(deviceToOpen) {
		if (!expectedDevice) {
			throw dv::exceptions::RuntimeError("Compatible camera callback must be defined.");
		}

		// Check descriptor is valid.
		const auto compat = expectedDevice(deviceToOpen.vid, deviceToOpen.pid, deviceToOpen.deviceType);

		if (!compat) {
			throw dv::exceptions::InvalidArgument<uint16_t>(
				"Invalid VID/PID/Device Type in opening descriptor (PID reported here).", deviceToOpen.pid);
		}

		if (deviceToOpen.cameraModel != compat.value()) {
			throw dv::exceptions::InvalidArgument<CameraModel>(
				"Invalid camera model in opening descriptor.", deviceToOpen.cameraModel);
		}

		// Build camera names.
		if (deviceToOpen.serialNumber.empty()) {
			mUsbThreadName = fmt::format("{}-{}", shortName, generateRepeatableSerialNumber(deviceToOpen));

			std::unique_lock lock{mUsbDeviceNameLock};
			mUsbDeviceName = fmt::format("{}_{}", longName, generateRepeatableSerialNumber(deviceToOpen));
		}
		else {
			mUsbThreadName = fmt::format("{}-{}", shortName, deviceToOpen.serialNumber);

			std::unique_lock lock{mUsbDeviceNameLock};
			mUsbDeviceName = fmt::format("{}_{}", longName, deviceToOpen.serialNumber);
		}

		// Initialize libusb using a separate context for each device.
		// This is to correctly support one thread per device.
		// libusb may create its own threads at this stage, so we temporarily set a different thread name.
		{
			dv::io::support::ThreadNameSwitch temporaryThreadNameSwitch{mUsbThreadName};

			int status;
			libusb_context *deviceContextCAPI{nullptr};

			// libusb debug generates too much noise.
			auto logLevelLibusb = getLogLevel();
			if (logLevelLibusb == LogLevel::LVL_DEBUG) {
				logLevelLibusb = LogLevel::LVL_INFO;
			}

#if (LIBUSB_API_VERSION >= DV_LIBUSB_VERSION_1_0_27_28)
			std::array<libusb_init_option, 2> initOptions{};

			initOptions[0].option          = LIBUSB_OPTION_LOG_LEVEL;
			initOptions[0].value.ival      = static_cast<libusb_log_level>(logLevelLibusb);
			initOptions[1].option          = LIBUSB_OPTION_LOG_CB;
			initOptions[1].value.log_cbval = libusbLogCallback;

			status = libusb_init_context(&deviceContextCAPI, initOptions.data(), initOptions.size());
#else
			status = libusb_init(&deviceContextCAPI);

			if (status == LIBUSB_SUCCESS) {
				libusb_set_option(
					deviceContextCAPI, LIBUSB_OPTION_LOG_LEVEL, static_cast<libusb_log_level>(logLevelLibusb));
				libusb_set_log_cb(deviceContextCAPI, libusbLogCallback, LIBUSB_LOG_CB_CONTEXT);
			}
#endif

			if (status != LIBUSB_SUCCESS) {
				throw dv::exceptions::RuntimeError(errorPrint("Failed to initialize libusb context.", status));
			}

			mDeviceContext.reset(deviceContextCAPI);

			{
				std::unique_lock lock{LIBUSB_LOG_MAP_LOCK};
				LIBUSB_LOG_MAP[deviceContextCAPI] = std::bind_front(&USBDevice::log, this);
			}
		}

		LibusbManagedDeviceList devicesList{nullptr, libusbDeviceListDeleter};
		size_t devicesListSize;

		{
			libusb_device **devicesListCAPI{nullptr};
			const auto result = libusb_get_device_list(mDeviceContext.get(), &devicesListCAPI);
			if (result < 0) {
				throw dv::exceptions::RuntimeError(errorPrint("Failed to list USB devices.", static_cast<int>(result)));
			}

			devicesList.reset(devicesListCAPI);
			devicesListSize = static_cast<size_t>(result);
		}

		// Cycle thorough all discovered devices and find a match. Only one can match the VID/PID/USB Bus Address tuple.
		for (size_t i = 0; i < devicesListSize; i++) {
			libusb_device_descriptor devDesc{};

			if (libusb_get_device_descriptor(devicesList[i], &devDesc) != LIBUSB_SUCCESS) {
				continue;
			}

			// Check if this is the device we want (VID/PID).
			if (devDesc.idVendor != deviceToOpen.vid) {
				continue;
			}

			if (devDesc.idProduct != deviceToOpen.pid) {
				continue;
			}

			if (const auto deviceType = dv::IntegerAsEnum<USBDeviceType>((devDesc.bcdDevice >> 8) & 0xFF);
				deviceType != deviceToOpen.deviceType) {
				continue;
			}

			if (libusb_get_bus_number(devicesList[i]) != deviceToOpen.busNumber) {
				continue;
			}

			if (libusb_get_device_address(devicesList[i]) != deviceToOpen.devAddress) {
				continue;
			}

			// Refresh device firmware version before opening, so that firmwareVersion
			// is always up-to-date. We only need the device descriptors for this.
			mDescriptor.firmwareVersion = (devDesc.bcdDevice & 0xFF);

			{
				libusb_device_handle *deviceHandleCAPI{nullptr};

				if (const auto status = libusb_open(devicesList[i], &deviceHandleCAPI); status != LIBUSB_SUCCESS) {
					// This is the only device that can match a specific USB bus address.
					// So if we fail here, we can stop and error out.
					throw dv::exceptions::RuntimeError(
						errorPrint("Failed to open USB device. This usually happens due to permissions or driver "
								   "issues, or because the device is already in use by another running program.",
							status));
				}

				mDeviceHandle.reset(deviceHandleCAPI);
			}

			if (devDesc.iSerialNumber == 0) {
				// Unknown serial number.
				mDescriptor.serialNumber = generateRepeatableSerialNumber(mDescriptor);
			}
			else {
				// Get the device's serial number.
				const auto serialNumber = fetchSerialNumber(mDeviceHandle.get(), devDesc.iSerialNumber);
				if (!serialNumber) {
					// This is the only device that can match a specific USB address.
					// So if we fail here, we can stop and error out.
					throw dv::exceptions::RuntimeError("Failed to get a valid USB serial number.");
				}

				mDescriptor.serialNumber = serialNumber.value();
			}

			// Update camera names.
			mUsbThreadName = fmt::format("{}-{}", shortName, mDescriptor.serialNumber);

			std::unique_lock lock{mUsbDeviceNameLock};
			mUsbDeviceName = fmt::format("{}_{}", longName, mDescriptor.serialNumber);

			break;
		}

		if (!mDeviceHandle) {
			throw dv::exceptions::RuntimeError(
				fmt::format("Could not find a device with VID={}, PID={} and USB Bus Address={}:{}.", deviceToOpen.vid,
					deviceToOpen.pid, deviceToOpen.busNumber, deviceToOpen.devAddress));
		}

		// Check the serial number restriction, if any is present.
		if (!deviceToOpen.serialNumber.empty() && (getSerialNumber() != deviceToOpen.serialNumber)) {
			// This is the only device that can match a specific USB address.
			// So if we fail here, we can stop and error out.
			throw dv::exceptions::RuntimeError(
				fmt::format("Failed to verify serial number (expected: '{}', found: '{}').", deviceToOpen.serialNumber,
					getSerialNumber()));
		}

		// Verify device firmware version.
		if (requiredFirmwareVersion >= 0) {
			if (getFirmwareVersion() != static_cast<uint8_t>(requiredFirmwareVersion)) {
				// This is the only device that can match a specific USB address.
				// So if we fail here, we can stop and error out.
				throw dv::exceptions::RuntimeError(
					fmt::format("Device firmware version incorrect. You have version {}; but version {} is required. "
								"Please update by following the Flashy documentation at "
								"'https://docs.inivation.com/hardware/hardware-advanced-usage/firmware-update.html'.",
						getFirmwareVersion(), requiredFirmwareVersion));
			}
		}

		// Check that the active configuration is set to number 1. If not, do so.
		// Then claim interface 0 (default).
		int activeConfiguration;
		if (const auto status = libusb_get_configuration(mDeviceHandle.get(), &activeConfiguration);
			status != LIBUSB_SUCCESS) {
			throw dv::exceptions::RuntimeError(errorPrint("Failed to get active USB configuration.", status));
		}

		if (activeConfiguration != 1) {
			if (const auto status = libusb_set_configuration(mDeviceHandle.get(), 1); status != LIBUSB_SUCCESS) {
				throw dv::exceptions::RuntimeError(errorPrint("Failed to set active USB configuration to 1.", status));
			}
		}

		// Claim interface 0 (default).
		if (const auto status = libusb_claim_interface(mDeviceHandle.get(), 0); status != LIBUSB_SUCCESS) {
			throw dv::exceptions::RuntimeError(
				errorPrint("Failed to claim USB interface 0. This usually happens because the device is already in use "
						   "by another running program.",
					status));
		}

		usbThreadStart();

		// Same log-level on the device, now that it's open and valid we can set it.
		try {
			usbControlTransferOut(VENDOR_REQUEST_LOG_LEVEL, dv::EnumAsInteger(getLogLevel()), 0, {});
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Unsupported on old devices.
		}

		// Cleanup stale data, needs to empty all buffers on data endpoint.
		cleanupBuffers();
	}

	~USBDevice() {
		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		usbThreadStop();

		// Release interface 0 (default).
		libusb_release_interface(mDeviceHandle.get(), 0);
	}

	void cleanupBuffers() {
		// Cleanup stale data, needs to empty all buffers on data endpoint.
		try {
			usbControlTransferOut(VENDOR_REQUEST_DATA_CLEANUP, 0, 0, {});
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Old way of cleaning up stale data via endpoint halt, which is technically abusing this mechanism,
			// and was discontinued.
			libusb_clear_halt(mDeviceHandle.get(), getDataEndpoint());
		}
	}

	// Async USB control exists to avoid the problem described in
	// https://sourceforge.net/p/libusb/mailman/message/34129129/
	// where the config function after the loop is never entered.
	void usbControlTransferOutAsync(const uint8_t bRequest, const uint16_t wValue, const uint16_t wIndex,
		const std::span<const uint8_t> data, controlOutCallbackType controlOutCallback) const {
		if (data.size() > MAX_CONTROL_TRANSFER_SIZE) {
			throw dv::exceptions::InvalidArgument<size_t>(
				"Invalid control transfer OUT data portion size.", data.size());
		}

		LibusbManagedTransfer controlTransfer{libusb_alloc_transfer(0), libusbTransferDeleter};
		if (!controlTransfer) {
			throw dv::exceptions::BadAlloc();
		}

		// Allocate data buffer.
		auto controlTransferBuffer = std::make_unique<controlOutBuffer>();
		if (!controlTransferBuffer) {
			throw dv::exceptions::BadAlloc();
		}

		// Set members.
		if (controlOutCallback) {
			controlTransferBuffer->callback = std::move(controlOutCallback);
		}
		if (!data.empty()) {
			memcpy(controlTransferBuffer->buffer, data.data(), data.size());
		}

		// Initialize Transfer.
		libusb_fill_control_setup(controlTransferBuffer->setup,
			(static_cast<uint8_t>(LIBUSB_ENDPOINT_OUT) | static_cast<uint8_t>(LIBUSB_REQUEST_TYPE_VENDOR)
				| static_cast<uint8_t>(LIBUSB_RECIPIENT_DEVICE)),
			bRequest, wValue, wIndex, data.size());

		libusb_fill_control_transfer(controlTransfer.get(), mDeviceHandle.get(), controlTransferBuffer->setup,
			&usbControlOutCallback, controlTransferBuffer.get(), 0);

		log(LogLevel::LVL_DEBUG,
			fmt::format(
				"Sending vendor request OUT with bRequest=0x{:02X}, wValue=0x{:04X}, wIndex=0x{:04X}, wLength={}",
				bRequest, wValue, wIndex, data.size()));

		if (const auto status = libusb_submit_transfer(controlTransfer.get()); status != LIBUSB_SUCCESS) {
			throw dv::exceptions::RuntimeError(errorPrint("Failed to submit USB transfer OUT", status));
		}

		// Release memory, callback will free it.
		controlTransfer.release();
		controlTransferBuffer.release();
	}

	void usbControlTransferInAsync(const uint8_t bRequest, const uint16_t wValue, const uint16_t wIndex,
		const size_t dataSize, controlInCallbackType controlInCallback) const {
		if (dataSize > MAX_CONTROL_TRANSFER_SIZE) {
			throw dv::exceptions::InvalidArgument<size_t>("Invalid control transfer IN data portion size.", dataSize);
		}

		LibusbManagedTransfer controlTransfer{libusb_alloc_transfer(0), libusbTransferDeleter};
		if (!controlTransfer) {
			throw dv::exceptions::BadAlloc();
		}

		// Allocate data buffer.
		auto controlTransferBuffer = std::make_unique<controlInBuffer>();
		if (!controlTransferBuffer) {
			throw dv::exceptions::BadAlloc();
		}

		// Set members.
		if (controlInCallback) {
			controlTransferBuffer->callback = std::move(controlInCallback);
		}

		// Initialize Transfer.
		libusb_fill_control_setup(controlTransferBuffer->setup,
			(static_cast<uint8_t>(LIBUSB_ENDPOINT_IN) | static_cast<uint8_t>(LIBUSB_REQUEST_TYPE_VENDOR)
				| static_cast<uint8_t>(LIBUSB_RECIPIENT_DEVICE)),
			bRequest, wValue, wIndex, dataSize);

		libusb_fill_control_transfer(controlTransfer.get(), mDeviceHandle.get(), controlTransferBuffer->setup,
			&usbControlInCallback, controlTransferBuffer.get(), 0);

		log(LogLevel::LVL_DEBUG,
			fmt::format(
				"Sending vendor request IN with bRequest=0x{:02X}, wValue=0x{:04X}, wIndex=0x{:04X}, wLength={}",
				bRequest, wValue, wIndex, dataSize));

		if (const auto status = libusb_submit_transfer(controlTransfer.get()); status != LIBUSB_SUCCESS) {
			throw dv::exceptions::RuntimeError(errorPrint("Failed to submit USB transfer IN", status));
		}

		// Release memory, callback will free it.
		controlTransfer.release();
		controlTransferBuffer.release();
	}

	// Implement synchronous API over asynchronous one: wait till callbacks are done.
	// This is done again here because the libusb API is unsuitable as it does its
	// own event handling, which we want to fully delegate to our USB thread.
	void usbControlTransferOut(const uint8_t bRequest, const uint16_t wValue, const uint16_t wIndex,
		const std::span<const uint8_t> data) const {
		std::atomic_int_fast32_t completed{-1};

		usbControlTransferOutAsync(
			bRequest, wValue, wIndex, data, [&completed](const enum libusb_transfer_status status) {
				completed = status;
			});

		// Request is out and will be handled at some point by USB thread, wait on that.
		while (completed < 0) {
			// Sleep for 100µs to avoid busy loop.
			std::this_thread::sleep_for(std::chrono::microseconds(100));
		}

		if (completed != LIBUSB_TRANSFER_COMPLETED) {
			// Failure.
			throw dv::exceptions::RuntimeError(
				errorPrint("Failed USB control transfer OUT", static_cast<int>(completed)));
		}
	}

	void usbControlTransferIn(
		const uint8_t bRequest, const uint16_t wValue, const uint16_t wIndex, const std::span<uint8_t> data) const {
		std::atomic_int_fast32_t completed{-1};

		usbControlTransferInAsync(bRequest, wValue, wIndex, data.size(),
			[&completed, &data](const enum libusb_transfer_status status, const std::span<const uint8_t> buffer) {
				if ((status == LIBUSB_TRANSFER_COMPLETED) && (buffer.size() == data.size())) {
					// Copy data to location given by user.
					memcpy(data.data(), buffer.data(), buffer.size());
				}

				completed = status;
			});

		// Request is out and will be handled at some point by USB thread, wait on that.
		while (completed < 0) {
			// Sleep for 100µs to avoid busy loop.
			std::this_thread::sleep_for(std::chrono::microseconds(100));
		}

		if (completed != LIBUSB_TRANSFER_COMPLETED) {
			// Failure.
			throw dv::exceptions::RuntimeError(
				errorPrint("Failed USB control transfer IN", static_cast<int>(completed)));
		}
	}

	void spiConfigSendMultiple(const std::span<SPIConfigurationParameters> configs) const {
		if (configs.empty()) {
			return;
		}

		for (auto &c : configs) {
			// Param must be in big-endian format.
			c.parameterValue = boost::endian::native_to_big(c.parameterValue);
		}

		log(LogLevel::LVL_DEBUG, fmt::format("Sending SPI Multiple config with length: {}", configs.size()));

		usbControlTransferOut(VENDOR_REQUEST_SPI_CONFIG_MULTIPLE, configs.size(), 0,
			{reinterpret_cast<uint8_t *>(configs.data()), (configs.size() * sizeof(SPIConfigurationParameters))});
	}

	void spiConfigSendMultipleAsync(
		const std::span<SPIConfigurationParameters> configs, controlOutCallbackType callback) const {
		if (configs.empty()) {
			return;
		}

		for (auto &c : configs) {
			// Param must be in big-endian format.
			c.parameterValue = boost::endian::native_to_big(c.parameterValue);
		}

		log(LogLevel::LVL_DEBUG, fmt::format("Sending Async SPI Multiple config with length: {}", configs.size()));

		usbControlTransferOutAsync(VENDOR_REQUEST_SPI_CONFIG_MULTIPLE, configs.size(), 0,
			{reinterpret_cast<uint8_t *>(configs.data()), (configs.size() * sizeof(SPIConfigurationParameters))},
			std::move(callback));
	}

	void spiConfigSend(const uint8_t moduleAddr, const uint16_t paramAddr, const uint32_t param) const {
		std::array<uint8_t, sizeof(uint32_t)> spiConfig{};
		boost::endian::store_big_u32(spiConfig.data(), param);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Sending SPI config to module {} with param {}: {}", moduleAddr, paramAddr, param));

		usbControlTransferOut(VENDOR_REQUEST_SPI_CONFIG, moduleAddr, paramAddr, spiConfig);
	}

	void spiConfigSendAsync(const uint8_t moduleAddr, const uint16_t paramAddr, const uint32_t param,
		controlOutCallbackType callback) const {
		std::array<uint8_t, sizeof(uint32_t)> spiConfig{};
		boost::endian::store_big_u32(spiConfig.data(), param);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Sending Async SPI config to module {} with param {}: {}", moduleAddr, paramAddr, param));

		usbControlTransferOutAsync(VENDOR_REQUEST_SPI_CONFIG, moduleAddr, paramAddr, spiConfig, std::move(callback));
	}

	[[nodiscard]] uint32_t spiConfigReceive(const uint8_t moduleAddr, const uint16_t paramAddr) const {
		std::array<uint8_t, sizeof(uint32_t)> param{};

		usbControlTransferIn(VENDOR_REQUEST_SPI_CONFIG, moduleAddr, paramAddr, param);

		const auto paramInt = boost::endian::load_big_u32(param.data());

		log(LogLevel::LVL_DEBUG,
			fmt::format("Received SPI config from module {} with param {}: {}", moduleAddr, paramAddr, paramInt));

		return paramInt;
	}

	void spiConfigReceiveAsync(const uint8_t moduleAddr, const uint16_t paramAddr,
		std::function<void(libusb_transfer_status status, uint32_t param)> callback) const {
		usbControlTransferInAsync(VENDOR_REQUEST_SPI_CONFIG, moduleAddr, paramAddr, sizeof(uint32_t),
			[this, moduleAddr, paramAddr, callback = std::move(callback)](
				const enum libusb_transfer_status status, const std::span<const uint8_t> buffer) {
				if (callback) {
					uint32_t param{0};

					if ((status == LIBUSB_TRANSFER_COMPLETED) && (buffer.size() == sizeof(uint32_t))) {
						param = boost::endian::load_big_u32(buffer.data());
					}

					log(LogLevel::LVL_DEBUG,
						fmt::format("Received Async SPI config from module {} with param {}: {} (status {})",
							moduleAddr, paramAddr, param, dv::EnumAsInteger(status)));

					callback(status, param);
				}
			});
	}

	void log(const LogLevel level, const std::string_view message) const {
		if (level > getLogLevel()) {
			return;
		}

		if (mUsbDeviceLogger) {
			std::shared_lock lock{mUsbDeviceNameLock};

			mUsbDeviceLogger(level, mUsbDeviceName, message);
		}
	}

	/**
	 * Update camera name base, append serial number after it.
	 * ONLY CALL IN CONSTRUCTOR!
	 * @param cameraName updated camera name base
	 */
	void resetCameraName(const std::string_view cameraName) {
		std::unique_lock lock{mUsbDeviceNameLock};
		mUsbDeviceName = fmt::format("{}_{}", cameraName, getSerialNumber());
	}

	/**
	 * Set data handling callback, receives the buffers filled with data from the BULK endpoint.
	 * Not thread-safe, call before usbThreadStart() only.
	 *
	 * @param callback data handling callback
	 */
	void setDataCallback(usbTransferCallbackType callback) {
		mUsbDataCallback = std::move(callback);
	}

	/**
	 * Set exceptional shutdown handling callback, called when the device goes away unexpectedly.
	 * Not thread-safe, call before usbThreadStart() only.
	 * Called from a context where locks are held, so it cannot call any of the USB data transfer control functions:
	 * usbThreadStart(), usbThreadStop(), usbDataTransfersStart(), usbDataTransfersStop(), setTransfersNumber(),
	 * getTransfersNumber(), setTransfersSize(), getTransfersSize().
	 * You should only use this to set some asynchronous notification flag to be safe!
	 *
	 * @param callback exceptional shutdown handling callback
	 */
	void setShutdownCallback(usbShutdownCallbackType callback) {
		mUsbShutdownCallback = std::move(callback);
	}

	/**
	 * Set debug handling callback, receives the buffers filled with debug data from the INTERRUPT endpoint.
	 * Not thread-safe, call before usbThreadStart() only.
	 *
	 * @param callback debug data handling callback
	 */
	void setDebugCallback(usbTransferCallbackType callback) {
		mUsbDebugCallback = std::move(callback);
	}

	/**
	 * Set data handling callback, receives the buffers filled with data from the BULK endpoint.
	 * Not thread-safe, call before usbThreadStart() only.
	 *
	 * @param dataEndPoint new data endpoint address
	 */
	void setDataEndpoint(const uint8_t dataEndPoint) {
		mDataEndPoint = dataEndPoint;
	}

	[[nodiscard]] uint8_t getDataEndpoint() const {
		return mDataEndPoint;
	}

	[[nodiscard]] bool isUSBThreadRunning() const {
		return mUsbThreadRun;
	}

	[[nodiscard]] bool isUSBDataTransferRunning() const {
		return mDataTransfersRun;
	}

	void usbThreadStart() {
		std::scoped_lock lock{mUsbOpsLock};

		if (isUSBThreadRunning()) {
			throw dv::exceptions::RuntimeError("USB thread already running.");
		}

		// Start USB thread.
		mUsbThread = std::thread(usbThreadFunc, this);

		// Wait for USB thread to be ready.
		while (!isUSBThreadRunning()) {
			std::this_thread::sleep_for(std::chrono::microseconds(100));
		}
	}

	void usbThreadStop() {
		std::scoped_lock lock{mUsbOpsLock};

		if (isUSBDataTransferRunning()) {
			throw dv::exceptions::RuntimeError("Data transfers must be stopped first.");
		}

		if (!isUSBThreadRunning()) {
			throw dv::exceptions::RuntimeError("USB thread already stopped.");
		}

		// Shut down USB thread.
		mUsbThreadRun = false;

		// Wait for USB thread to terminate.
		mUsbThread.join();
	}

	void usbDataTransfersStart() {
		std::scoped_lock lock{mUsbOpsLock, mDataTransfersLock};

		if (!isUSBThreadRunning()) {
			throw dv::exceptions::RuntimeError("USB thread must be running first.");
		}

		if (isUSBDataTransferRunning()) {
			throw dv::exceptions::RuntimeError("Data transfers already running.");
		}

		usbAllocateDataTransfersNLCK();
	}

	void usbDataTransfersStop() {
		std::scoped_lock lock{mUsbOpsLock, mDataTransfersLock};

		usbCancelAndDeallocateDataTransfersNLCK();
	}

	void usbDebugTransfersStart() {
		std::scoped_lock lock{mUsbOpsLock};

		if (!isUSBThreadRunning()) {
			throw dv::exceptions::RuntimeError("USB thread must be running first.");
		}

		usbAllocateDebugTransfers();
	}

	void usbDebugTransfersStop() {
		std::scoped_lock lock{mUsbOpsLock};

		usbCancelAndDeallocateDebugTransfers();
	}

private:
	static constexpr size_t MAX_CONTROL_TRANSFER_SIZE{4 * 1024};

	struct controlOutBuffer {
		uint8_t setup[LIBUSB_CONTROL_SETUP_SIZE]{};
		uint8_t buffer[MAX_CONTROL_TRANSFER_SIZE]{};
		controlOutCallbackType callback{};
	};

	// setup and buffer need to be contiguous in memory.
	static_assert(offsetof(controlOutBuffer, setup) == 0);
	static_assert(offsetof(controlOutBuffer, buffer) == LIBUSB_CONTROL_SETUP_SIZE);

	struct controlInBuffer {
		uint8_t setup[LIBUSB_CONTROL_SETUP_SIZE]{};
		uint8_t buffer[MAX_CONTROL_TRANSFER_SIZE]{};
		controlInCallbackType callback{};
	};

	// setup and buffer need to be contiguous in memory.
	static_assert(offsetof(controlInBuffer, setup) == 0);
	static_assert(offsetof(controlInBuffer, buffer) == LIBUSB_CONTROL_SETUP_SIZE);

	static_assert(LIBUSB_API_VERSION >= DV_LIBUSB_VERSION_1_0_23, "libusb 1.0.23 or newer is required.");

	static constexpr uint8_t MAX_SERIAL_NUMBER_LENGTH{8};
	static constexpr uint8_t DEFAULT_DATA_ENDPOINT{0x82};
	static constexpr uint8_t DEBUG_ENDPOINT{0x81};
	static constexpr uint32_t DEBUG_TRANSFERS_NUMBER{8};
	static constexpr uint32_t DEBUG_TRANSFERS_SIZE{64};

	static constexpr uint8_t VENDOR_REQUEST_LOG_LEVEL{0xB1};
	static constexpr uint8_t VENDOR_REQUEST_DATA_CLEANUP{0xC6};
	static constexpr uint8_t VENDOR_REQUEST_SPI_CONFIG_MULTIPLE{0xC2};

	static void LIBUSB_CALL usbControlOutCallback(struct libusb_transfer *transfer) {
		auto *controlTransferBuffer = static_cast<controlOutBuffer *>(transfer->user_data);

		if (controlTransferBuffer->callback) {
			controlTransferBuffer->callback(transfer->status);
		}

		libusb_free_transfer(transfer);
		delete controlTransferBuffer;
	}

	static void LIBUSB_CALL usbControlInCallback(struct libusb_transfer *transfer) {
		auto *controlTransferBuffer = static_cast<controlInBuffer *>(transfer->user_data);

		if (controlTransferBuffer->callback) {
			controlTransferBuffer->callback(transfer->status,
				{libusb_control_transfer_get_data(transfer), static_cast<size_t>(transfer->actual_length)});
		}

		libusb_free_transfer(transfer);
		delete controlTransferBuffer;
	}

	static inline std::shared_mutex LIBUSB_LOG_MAP_LOCK{};
	static inline std::unordered_map<libusb_context *, std::function<void(LogLevel level, std::string_view message)>>
		LIBUSB_LOG_MAP{};

	static void LIBUSB_CALL libusbLogCallback(libusb_context *ctx, const enum libusb_log_level level, const char *str) {
		std::shared_lock lock{LIBUSB_LOG_MAP_LOCK};

		try {
			LIBUSB_LOG_MAP.at(ctx)(static_cast<LogLevel>(level), str);
		}
		catch (const std::out_of_range &) {
			// Ignore.
		}
	}

	static std::string errorPrint(const std::string_view msg, const int code) {
		return fmt::format("{} Error code: {}.", msg, libusb_error_name(code));
	}

	// Generate a repeatable serial number valid for this session using bus and device addresses.
	static std::string generateRepeatableSerialNumber(const DeviceDescriptor &desc) {
		const uint16_t serialNumberNotAvailable = (desc.busNumber << 8) | desc.devAddress;

		return fmt::format("TMP{:05}", serialNumberNotAvailable);
	}

	static std::optional<std::string> fetchSerialNumber(
		libusb_device_handle *const handle, const uint8_t serialNumberIndex) {
		// Get the device's serial number.
		std::string serialNumber{};
		serialNumber.resize(MAX_SERIAL_NUMBER_LENGTH + 1);

		auto getStringDescResult = libusb_get_string_descriptor_ascii(handle, serialNumberIndex,
			reinterpret_cast<uint8_t *>(serialNumber.data()), static_cast<int>(serialNumber.size()));

		// Check serial number success and length.
		if (getStringDescResult < 0) {
			return std::nullopt;
		}

		// Use received string.
		if (getStringDescResult > MAX_SERIAL_NUMBER_LENGTH) {
			getStringDescResult = MAX_SERIAL_NUMBER_LENGTH;
		}

		serialNumber.resize(getStringDescResult);

		return serialNumber;
	}

	// This thread handles all USB events exclusively, from when it starts up
	// after device open to when it shuts down at device close.
	static void usbThreadFunc(USBDevice *state) {
		state->log(LogLevel::LVL_DEBUG, "USB thread starting ...");

		// Set thread name.
		dv::io::support::ThreadExtra::setName(state->mUsbThreadName);

		// Raise thread priority.
		dv::io::support::ThreadExtra::setPriorityHighest();

		// Signal data thread ready back to start function.
		state->mUsbThreadRun = true;

		state->log(LogLevel::LVL_DEBUG, "USB thread running.");

		// Handle USB events (10 millisecond timeout).
		struct timeval te = {.tv_sec = 0, .tv_usec = 10000};

		while (state->isUSBThreadRunning()) {
			if (const auto status = libusb_handle_events_timeout_completed(state->mDeviceContext.get(), &te, nullptr);
				status != LIBUSB_SUCCESS) {
				state->log(LogLevel::LVL_WARNING, errorPrint("USB event handling failed.", status));
			}
		}

		state->log(LogLevel::LVL_DEBUG, "USB thread shut down.");
	}

	static void LIBUSB_CALL usbDataTransferCallback(struct libusb_transfer *transfer) {
		auto *state = static_cast<USBDevice *>(transfer->user_data);

		// Completed or cancelled transfers are what we expect to handle here, so
		// if they do have data attached, try to parse them.
		if (((transfer->status == LIBUSB_TRANSFER_COMPLETED) || (transfer->status == LIBUSB_TRANSFER_CANCELLED))
			&& (transfer->actual_length > 0)) {
			// Handle data.
			if (state->mUsbDataCallback) {
				state->mUsbDataCallback({transfer->buffer, static_cast<size_t>(transfer->actual_length)});
			}

			state->log(
				LogLevel::LVL_DEBUG, fmt::format("Got USB data buffer with length: {}.", transfer->actual_length));
		}

		// Only status that indicates a new transfer can be really submitted is
		// COMPLETED. TIMED_OUT is impossible, and ERROR/STALL/NO_DEVICE/CANCELLED
		// are not recoverable, as all of them appear on different OSes when a
		// device is physically unplugged for example.
		if (transfer->status == LIBUSB_TRANSFER_COMPLETED) {
			// Submit transfer again.
			const auto status = libusb_submit_transfer(transfer);
			if (status == LIBUSB_SUCCESS) {
				return;
			}

			state->log(LogLevel::LVL_DEBUG, errorPrint("Failed to re-submit USB transfer.", status));
		}

		// Cannot recover (cancelled, no device, or other critical error).
		// Signal this by adjusting the counters and exiting.
		// Freeing the transfers is taken care of by usbCancelAndDeallocateTransfersNLCK().
		// 'activeDataTransfers' drops to zero in three cases:
		// - the device went away
		// - the data transfers were stopped, which cancels all transfers
		// - the USB buffer number/size changed, which cancels all transfers
		// The second and third case are intentional user actions, so we don't notify.
		// In the first case, the last transfer to go away calls the shutdown
		// callback and notifies that we're exiting, and not via normal cancellation.
		// On macOS, when an error occurs (like device being unplugged), not all
		// transfers return with the expected error code, but only one, the rest returns
		// LIBUSB_TRANSFER_CANCELLED, which we only expect on actual user cancel actions.
		// To track this, we count the transfers that did fail with any other error code,
		// and use that information to determine if the shutdown was intentional by the
		// user or not. If not, at least one transfer would fail with a non-cancel error.
		state->log(LogLevel::LVL_DEBUG, errorPrint("USB transfer failure.", transfer->status));

		{
			std::scoped_lock lock{state->mDataTransfersLock};

			state->mDataTransfersActiveNLCK--;

			if (transfer->status != LIBUSB_TRANSFER_CANCELLED) {
				// This also captures COMPLETED but with re-submit failure (see above).
				state->mDataTransfersFailedNLCK++;
			}

			if (state->mDataTransfersActiveNLCK == 0) {
				state->log(LogLevel::LVL_INFO, "All data transfers gone.");
			}

			// Transfers are handled sequentially always in the same thread, so we can wait on the last transfer to go
			// away and handle it as exceptional if it fits the criteria above.
			if ((state->mDataTransfersActiveNLCK == 0) && (state->mDataTransfersFailedNLCK > 0)) {
				// Ensure run is set to false on exceptional shut-down before callback.
				state->mDataTransfersRun = false;

				// Call exceptional shut-down callback. Lock is held!
				if (state->mUsbShutdownCallback) {
					state->mUsbShutdownCallback();
				}

				state->log(LogLevel::LVL_INFO, fmt::format("Exceptional shutdown completed, failed transfers: {}.",
												   state->mDataTransfersFailedNLCK));

				// Clear error tracking counter on exit.
				state->mDataTransfersFailedNLCK = 0;
			}
		}
	}

	static void LIBUSB_CALL usbDebugTransferCallback(struct libusb_transfer *transfer) {
		auto *state = static_cast<USBDevice *>(transfer->user_data);

		// Completed or cancelled transfers are what we expect to handle here, so
		// if they do have data attached, try to parse them.
		if (((transfer->status == LIBUSB_TRANSFER_COMPLETED) || (transfer->status == LIBUSB_TRANSFER_CANCELLED))
			&& (transfer->actual_length > 0)) {
			// Handle data. First handle log messages, then pass on to callback.
			const std::span<const uint8_t> data{transfer->buffer, static_cast<size_t>(transfer->actual_length)};

			if (data[0] == 0x00) {
				// Log message.
				state->log(LogLevel::LVL_ERROR, fmt::format("Device message: '{}' (code {} at device time {}).",
													std::string_view{reinterpret_cast<const char *>(&data[6])}, data[1],
													boost::endian::load_little_u32(&data[2])));
			}
			else {
				// Other message, forward to callback.
				if (state->mUsbDebugCallback) {
					state->mUsbDebugCallback(data);
				}
			}

			state->log(
				LogLevel::LVL_DEBUG, fmt::format("EP1: Got USB data buffer with length: {}.", transfer->actual_length));
		}

		// Only status that indicates a new transfer can be really submitted is
		// COMPLETED. TIMED_OUT is impossible, and ERROR/STALL/NO_DEVICE/CANCELLED
		// are not recoverable, as all of them appear on different OSes when a
		// device is physically unplugged for example.
		if (transfer->status == LIBUSB_TRANSFER_COMPLETED) {
			// Submit transfer again.
			const auto status = libusb_submit_transfer(transfer);
			if (status == LIBUSB_SUCCESS) {
				return;
			}

			state->log(LogLevel::LVL_DEBUG, errorPrint("EP1: Failed to re-submit USB transfer.", status));
		}

		// Cannot recover (cancelled, no device, or other critical error).
		// Signal this by adjusting the counters and exiting.
		// Freeing the transfers is taken care of by usbCancelAndDeallocateDebugTransfers().
		state->log(LogLevel::LVL_DEBUG, errorPrint("EP1: USB transfer failure.", transfer->status));

		state->mDebugTransfersActive--;

		if (state->mDebugTransfersActive == 0) {
			state->log(LogLevel::LVL_INFO, "EP1: All debug transfers gone.");
		}
	}

	static constexpr auto libusbContextDeleter = [](libusb_context *ctx) {
		libusb_exit(ctx);

		{
			std::unique_lock lock{LIBUSB_LOG_MAP_LOCK};
			LIBUSB_LOG_MAP.erase(ctx);
		}
	};

	static constexpr auto libusbDeviceHandleDeleter = [](libusb_device_handle *handle) {
		libusb_close(handle);
	};

	static constexpr auto libusbDeviceListDeleter = [](libusb_device **list) {
		libusb_free_device_list(list, true);
	};

	static constexpr auto libusbTransferDeleter = [](libusb_transfer *transfer) {
		libusb_free_transfer(transfer);
	};

	using LibusbManagedContext      = std::unique_ptr<libusb_context, decltype(libusbContextDeleter)>;
	using LibusbManagedDeviceHandle = std::unique_ptr<libusb_device_handle, decltype(libusbDeviceHandleDeleter)>;
	using LibusbManagedDeviceList   = std::unique_ptr<libusb_device *[], decltype(libusbDeviceListDeleter)>;
	using LibusbManagedTransfer     = std::unique_ptr<libusb_transfer, decltype(libusbTransferDeleter)>;

	// Per-device log-level (USB functions)
	std::atomic<LogLevel> mUsbLogLevel{LogLevel::LVL_WARNING};
	mutable std::shared_mutex mUsbDeviceNameLock{};
	std::string mUsbDeviceName{};
	loggerCallbackType mUsbDeviceLogger{};
	// USB Device State
	LibusbManagedContext mDeviceContext{nullptr, libusbContextDeleter};
	LibusbManagedDeviceHandle mDeviceHandle{nullptr, libusbDeviceHandleDeleter};
	DeviceDescriptor mDescriptor{};
	// USB thread state
	std::string mUsbThreadName{};
	std::thread mUsbThread{};
	std::atomic<bool> mUsbThreadRun{false};
	mutable std::mutex mUsbOpsLock{};
	// USB Data Transfers
	uint8_t mDataEndPoint{DEFAULT_DATA_ENDPOINT};
	std::atomic<bool> mDataTransfersRun{false}; // LOCK PROTECTED ON WRITE ONLY.
	mutable std::mutex mDataTransfersLock{};
	std::vector<LibusbManagedTransfer> mDataTransfersNLCK{}; // LOCK PROTECTED.
	uint32_t mDataTransfersActiveNLCK{0};                    // LOCK PROTECTED.
	uint32_t mDataTransfersFailedNLCK{0};                    // LOCK PROTECTED.
	uint32_t mDataTransfersNumberNLCK{32};                   // LOCK PROTECTED.
	uint32_t mDataTransfersSizeNLCK{8 * 1024};               // LOCK PROTECTED.
	// USB Data Transfers handling callback
	usbTransferCallbackType mUsbDataCallback{};
	// USB Data Transfers shutdown callback
	usbShutdownCallbackType mUsbShutdownCallback{};
	// Debug endpoint transfers.
	std::vector<LibusbManagedTransfer> mDebugTransfers{};
	std::atomic<uint32_t> mDebugTransfersActive{0};
	usbTransferCallbackType mUsbDebugCallback{};

	std::vector<LibusbManagedTransfer> usbAllocateTransfers(const size_t transfersNumber, const size_t transfersSize,
		const uint8_t endpoint, const libusb_transfer_type endpointType, const libusb_transfer_cb_fn callback) {
		std::vector<LibusbManagedTransfer> transfers{};

		// Allocate transfers and set them up.
		for (size_t i = 0; i < transfersNumber; i++) {
			LibusbManagedTransfer transfer{libusb_alloc_transfer(0), libusbTransferDeleter};
			if (!transfer) {
				log(LogLevel::LVL_ERROR,
					fmt::format("Unable to allocate libusb transfer ({} of {}).", i, transfersNumber));

				// No sense in continuing, memory exhausted, so more allocations will fail.
				break;
			}

			// Create data buffer. Use malloc() here to take advantage of LIBUSB_TRANSFER_FREE_BUFFER flag later, which
			// will free() the buffer memory for us.
			transfer->buffer = static_cast<uint8_t *>(malloc(transfersSize));
			if (transfer->buffer == nullptr) {
				log(LogLevel::LVL_ERROR,
					fmt::format("Unable to allocate buffer for libusb transfer ({} of {}).", i, transfersNumber));

				// No sense in continuing, memory exhausted, so more allocations will fail.
				break;
			}

			// Initialize Transfer.
			transfer->length     = static_cast<int>(transfersSize);
			transfer->dev_handle = mDeviceHandle.get();
			transfer->endpoint   = endpoint;
			transfer->type       = endpointType;
			transfer->callback   = callback;
			transfer->user_data  = this;
			transfer->timeout    = 0;
			transfer->flags      = LIBUSB_TRANSFER_FREE_BUFFER;

			if (const auto status = libusb_submit_transfer(transfer.get()); status != LIBUSB_SUCCESS) {
				log(LogLevel::LVL_ERROR,
					errorPrint(
						fmt::format("Unable to submit libusb transfer ({} of {}).", i, transfersNumber), status));

				// Try next one, maybe better luck ...
				continue;
			}

			// Success, one more transfer submitted and active!
			transfers.push_back(std::move(transfer));
		}

		if (transfers.empty()) {
			log(LogLevel::LVL_ERROR, "Unable to allocate any libusb transfers. Transfers stopped.");

			throw dv::exceptions::RuntimeError("Unable to allocate any libusb transfers.");
		}

		return transfers;
	}

	void usbCancelTransfers(std::vector<LibusbManagedTransfer> &transfers) {
		// Continue trying to cancel all transfers until there are none left.
		for (auto &transfer : transfers) {
			if (const auto status = libusb_cancel_transfer(transfer.get());
				(status != LIBUSB_SUCCESS) && (status != LIBUSB_ERROR_NOT_FOUND)) {
				log(LogLevel::LVL_ERROR, errorPrint("Unable to cancel libusb transfer.", status));
				// Proceed with trying to cancel all transfers regardless of errors.
			}
		}
	}

	// MUST LOCK ON 'mDataTransfersLock'.
	void usbAllocateDataTransfersNLCK() {
		mDataTransfersNLCK = usbAllocateTransfers(mDataTransfersNumberNLCK, mDataTransfersSizeNLCK, getDataEndpoint(),
			LIBUSB_TRANSFER_TYPE_BULK, &usbDataTransferCallback);
		mDataTransfersActiveNLCK = mDataTransfersNLCK.size();

		// Some transfers started!
		mDataTransfersRun = true;
	}

	// MUST LOCK ON 'mDataTransfersLock'.
	void usbCancelAndDeallocateDataTransfersNLCK() {
		mDataTransfersRun = false;

		// Wait for all transfers to go away.
		while (mDataTransfersActiveNLCK > 0) {
			// Continue trying to cancel all transfers until there are none left.
			// It seems like one cancel pass is not enough and some hang around.
			usbCancelTransfers(mDataTransfersNLCK);

			// Sleep for 10ms to avoid busy loop. Manually unlocking transfer lock so that the callback can run and
			// update 'mDataTransfersActiveNLCK'. This is fine as all functions calling this one must hold the lock,
			// and we always lock it again since sleep_for() cannot fail or throw, so all lock pre-conditions of this
			// function are again guaranteed as post-conditions. All operations changing USB data transfer state also at
			// a high-level own the USB ops lock and acquire it before the data transfer one, so no weird interleaving
			// is possible at a higher level.
			mDataTransfersLock.unlock();
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
			mDataTransfersLock.lock();
		}

		// No more transfers in flight, deallocate them all here, thanks to std::unique_ptr's deleter.
		// This will also free all buffers thanks to the LIBUSB_TRANSFER_FREE_BUFFER flag.
		mDataTransfersNLCK.clear();
	}

	void usbAllocateDebugTransfers() {
		mDebugTransfers       = usbAllocateTransfers(DEBUG_TRANSFERS_NUMBER, DEBUG_TRANSFERS_SIZE, DEBUG_ENDPOINT,
			LIBUSB_TRANSFER_TYPE_INTERRUPT, &usbDebugTransferCallback);
		mDebugTransfersActive = mDebugTransfers.size();
	}

	void usbCancelAndDeallocateDebugTransfers() {
		// Wait for all transfers to go away.
		while (mDebugTransfersActive > 0) {
			// Continue trying to cancel all transfers until there are none left.
			// It seems like one cancel pass is not enough and some hang around.
			usbCancelTransfers(mDebugTransfers);

			// Sleep for 10ms to avoid busy loop.
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
		}

		// No more transfers in flight, deallocate them all here, thanks to std::unique_ptr's deleter.
		// This will also free all buffers thanks to the LIBUSB_TRANSFER_FREE_BUFFER flag.
		mDebugTransfers.clear();
	}
};
} // namespace dv::io::camera

// fmt compatibility for enum class printing.
template<>
struct fmt::formatter<dv::io::camera::USBDevice::LogLevel> : fmt::ostream_formatter {};
