#pragma once

#include "../../exception/exceptions/generic_exceptions.hpp"
#include "davis.hpp"
#include "dvs128.hpp"
#include "dvxplorer.hpp"
#include "dvxplorer_m.hpp"

namespace dv::io::camera {

/**
 * Discover all compatible cameras connected to this system.
 * @param logger discovery logging callback, must be thread-safe
 * @return list of device descriptor structures
 */
[[nodiscard]] static std::vector<USBDevice::DeviceDescriptor> discover(
	const USBDevice::loggerCallbackType &logger = {}) {
	const auto dvs128      = DVS128::findDevices({}, logger);
	const auto davis       = DAVIS::findDevices({}, logger);
	const auto dvxplorer   = DVXplorer::findDevices({}, logger);
	const auto dvxplorer_m = DVXplorerM::findDevices({}, logger);

	std::vector<USBDevice::DeviceDescriptor> devices{};

	devices.insert(devices.end(), dvs128.cbegin(), dvs128.cend());
	devices.insert(devices.end(), davis.cbegin(), davis.cend());
	devices.insert(devices.end(), dvxplorer.cbegin(), dvxplorer.cend());
	devices.insert(devices.end(), dvxplorer_m.cbegin(), dvxplorer_m.cend());

	return devices;
}

/**
 * Open the device with the specified descriptor structure.
 * Throws if device described cannot be opened.
 * The generic pointer returned only supports some functionality that all cameras implement.
 * For more specific functionality, open the cameras directly via their class constructors, or up-cast with
 * std::dynamic_cast.
 *
 * @param descriptor device descriptor to try opening
 * @param deviceLogger per-device logging callback, must be thread-safe
 * @param deviceLogLevel initial log-level
 * @return generic pointer to camera
 */
[[nodiscard]] static CameraPtr open(const USBDevice::DeviceDescriptor &descriptor,
	USBDevice::loggerCallbackType deviceLogger = {},
	const USBDevice::LogLevel deviceLogLevel   = USBDevice::LogLevel::LVL_WARNING) {
	switch (descriptor.cameraModel) {
		case CameraModel::DVS128:
			return std::make_unique<DVS128>(descriptor, std::move(deviceLogger), deviceLogLevel);

		case CameraModel::DAVIS:
			return std::make_unique<DAVIS>(descriptor, std::move(deviceLogger), deviceLogLevel);

		case CameraModel::DVXPLORER:
			return std::make_unique<DVXplorer>(descriptor, std::move(deviceLogger), deviceLogLevel);

		case CameraModel::DVXPLORER_M:
			return std::make_unique<DVXplorerM>(descriptor, std::move(deviceLogger), deviceLogLevel);

		default:
			throw dv::exceptions::RuntimeError{"Unsupported camera model"};
	}
}

/**
 * Open the device with the specified serial number.
 * Throws if device described cannot be opened.
 * The generic pointer returned only supports some functionality that all cameras implement.
 * For more specific functionality, open the cameras directly via their class constructors, or up-cast with
 * std::dynamic_cast.
 *
 * @param serialNumber device serial number to try opening
 * @param deviceLogger per-device logging callback, must be thread-safe
 * @param deviceLogLevel initial log-level
 * @return generic pointer to camera
 */
[[nodiscard]] static CameraPtr open(const std::string_view serialNumber,
	USBDevice::loggerCallbackType deviceLogger = {},
	const USBDevice::LogLevel deviceLogLevel   = USBDevice::LogLevel::LVL_WARNING) {
	const auto devices = discover();

	const auto iter
		= std::find_if(devices.begin(), devices.end(), [serialNumber](const USBDevice::DeviceDescriptor &elem) {
			  return serialNumber.empty() || (elem.serialNumber == serialNumber);
		  });
	if (iter == devices.end()) {
		throw dv::exceptions::RuntimeError{"Could not find device with given serial number"};
	}

	return open(*iter, std::move(deviceLogger), deviceLogLevel);
}

/**
 * Open the first device that can be found.
 * Throws if device cannot be opened.
 * The generic pointer returned only supports some functionality that all cameras implement.
 * For more specific functionality, open the cameras directly via their class constructors, or up-cast with
 * std::dynamic_cast.
 *
 * @param deviceLogger per-device logging callback, must be thread-safe
 * @param deviceLogLevel initial log-level
 * @return generic pointer to camera
 */
[[nodiscard]] static CameraPtr open(USBDevice::loggerCallbackType deviceLogger = {},
	const USBDevice::LogLevel deviceLogLevel                                   = USBDevice::LogLevel::LVL_WARNING) {
	const auto devices = discover();

	return open(devices.at(0), std::move(deviceLogger), deviceLogLevel);
}

/**
 * Open the device with the specified descriptor structure.
 * Device must support clock synchronization.
 * Throws if device described cannot be opened.
 * The generic pointer returned only supports some functionality that all cameras implement.
 * For more specific functionality, open the cameras directly via their class constructors, or up-cast with
 * std::dynamic_cast.
 *
 * @param descriptor device descriptor to try opening
 * @param deviceLogger per-device logging callback, must be thread-safe
 * @param deviceLogLevel initial log-level
 * @return generic pointer to camera
 */
[[nodiscard]] static SyncCameraPtr openSync(const USBDevice::DeviceDescriptor &descriptor,
	USBDevice::loggerCallbackType deviceLogger = {},
	const USBDevice::LogLevel deviceLogLevel   = USBDevice::LogLevel::LVL_WARNING) {
	switch (descriptor.cameraModel) {
		case CameraModel::DVS128:
			return std::make_unique<DVS128>(descriptor, std::move(deviceLogger), deviceLogLevel);

		case CameraModel::DAVIS:
			return std::make_unique<DAVIS>(descriptor, std::move(deviceLogger), deviceLogLevel);

		case CameraModel::DVXPLORER:
			return std::make_unique<DVXplorer>(descriptor, std::move(deviceLogger), deviceLogLevel);

		default:
			throw dv::exceptions::RuntimeError{"Unsupported synchronizable camera model"};
	}
}

/**
 * Open the device with the specified serial number.
 * Device must support clock synchronization.
 * Throws if device described cannot be opened.
 * The generic pointer returned only supports some functionality that all cameras implement.
 * For more specific functionality, open the cameras directly via their class constructors, or up-cast with
 * std::dynamic_cast.
 *
 * @param serialNumber device serial number to try opening
 * @param deviceLogger per-device logging callback, must be thread-safe
 * @param deviceLogLevel initial log-level
 * @return generic pointer to camera
 */
[[nodiscard]] static SyncCameraPtr openSync(const std::string_view serialNumber,
	USBDevice::loggerCallbackType deviceLogger = {},
	const USBDevice::LogLevel deviceLogLevel   = USBDevice::LogLevel::LVL_WARNING) {
	const auto devices = discover();

	const auto iter
		= std::find_if(devices.begin(), devices.end(), [serialNumber](const USBDevice::DeviceDescriptor &elem) {
			  return serialNumber.empty() || (elem.serialNumber == serialNumber);
		  });
	if (iter == devices.end()) {
		throw dv::exceptions::RuntimeError{"Could not find device with given serial number"};
	}

	return openSync(*iter, std::move(deviceLogger), deviceLogLevel);
}

} // namespace dv::io::camera
