#pragma once

#include "imu_support.hpp"
#include "parsers/dvxplorer_parser.hpp"
#include "sync_camera_input_base.hpp"
#include "usb_device.hpp"

namespace dv::io::camera {
class DVXplorer final : public USBDevice, public SyncCameraInputBase {
public:
	enum class SubSample {
		EVERY_PIXEL   = 0,
		EVERY_SECOND  = 1,
		BAND_OF_TWO   = 2,
		EVERY_FOURTH  = 3,
		BAND_OF_FOUR  = 4,
		BIN_1010_0000 = 5,
		BIN_1100_0000 = 6,
		EVERY_EIGHTH  = 7,
	};

	friend std::ostream &operator<<(std::ostream &os, const SubSample &var) {
		switch (var) {
			case SubSample::EVERY_PIXEL:
				return os << "Every pixel (1111'1111)";

			case SubSample::EVERY_SECOND:
				return os << "Every second pixel (1010'1010)";

			case SubSample::BAND_OF_TWO:
				return os << "Band of two pixels (1100'1100)";

			case SubSample::EVERY_FOURTH:
				return os << "Every fourth pixel (1000'1000)";

			case SubSample::BAND_OF_FOUR:
				return os << "Band of four pixels (1111'0000)";

			case SubSample::BIN_1010_0000:
				return os << "(1010'0000)";

			case SubSample::BIN_1100_0000:
				return os << "(1100'0000)";

			case SubSample::EVERY_EIGHTH:
				return os << "Every eighth pixel (1000'0000)";
		}

		return os;
	}

	enum class ReadoutFPS {
		CONSTANT_100         = 0,
		CONSTANT_200         = 1,
		CONSTANT_500         = 2,
		CONSTANT_1000        = 3,
		CONSTANT_LOSSY_2000  = 4,
		CONSTANT_LOSSY_5000  = 5,
		CONSTANT_LOSSY_10000 = 6,
		VARIABLE_2000        = 7,
		VARIABLE_5000        = 8,
		VARIABLE_10000       = 9,
		VARIABLE_15000       = 10,
	};

	friend std::ostream &operator<<(std::ostream &os, const ReadoutFPS &var) {
		switch (var) {
			case ReadoutFPS::CONSTANT_100:
				return os << "100 fps constant";

			case ReadoutFPS::CONSTANT_200:
				return os << "200 fps constant";

			case ReadoutFPS::CONSTANT_500:
				return os << "500 fps constant";

			case ReadoutFPS::CONSTANT_1000:
				return os << "1000 fps constant";

			case ReadoutFPS::CONSTANT_LOSSY_2000:
				return os << "2000 fps constant, lossy";

			case ReadoutFPS::CONSTANT_LOSSY_5000:
				return os << "5000 fps constant, lossy";

			case ReadoutFPS::CONSTANT_LOSSY_10000:
				return os << "10000 fps constant, lossy";

			case ReadoutFPS::VARIABLE_2000:
				return os << "2000 fps variable";

			case ReadoutFPS::VARIABLE_5000:
				return os << "5000 fps variable";

			case ReadoutFPS::VARIABLE_10000:
				return os << "10000 fps variable";

			case ReadoutFPS::VARIABLE_15000:
				return os << "15000 fps variable";
		}

		return os;
	}

	/**
	 * Find connected DVXplorer cameras.
	 * @param filterBySerialNumber only search for devices with this serial number
	 * @param logger discovery logging callback, must be thread-safe
	 * @return a descriptor structure describing a compatible device
	 */
	[[nodiscard]] static auto findDevices(
		const std::string_view filterBySerialNumber = {}, loggerCallbackType logger = {}) {
		const auto found = findCompatibleDevices(COMPATIBLE_CAMERA, std::move(logger));

		return filterCompatibleDevices(found, filterBySerialNumber);
	}

	/**
	 * Open the first DVXplorer camera that can be found.
	 * Throws if device cannot be opened.
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVXplorer(loggerCallbackType deviceLogger = {}, const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVXplorer(findDevices().at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVXplorer camera with the specified serial number.
	 * Throws if device cannot be opened.
	 * @param filterBySerialNumber serial number to search for
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVXplorer(const std::string_view filterBySerialNumber, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVXplorer(findDevices(filterBySerialNumber).at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVXplorer camera corresponding to the specified descriptor.
	 * Throws if device cannot be opened.
	 * @param deviceToOpen device descriptor structure
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVXplorer(const DeviceDescriptor &deviceToOpen, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		USBDevice(deviceToOpen, std::move(deviceLogger), deviceLogLevel, "dvx", "DVXplorer", COMPATIBLE_CAMERA,
			FIRMWARE_REQUIRED_VERSION) {
		// Verify device logic version.
		mLogicVersion = spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_VERSION);

		if (mLogicVersion != LOGIC_REQUIRED_VERSION) {
			throw dv::exceptions::RuntimeError(
				fmt::format("Device logic 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'.",
					mLogicVersion, LOGIC_REQUIRED_VERSION));
		}

		// Verify device logic patch release.
		mLogicPatch = spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_PATCH);

		if (mLogicPatch < LOGIC_MINIMUM_PATCH) {
			throw dv::exceptions::RuntimeError(
				fmt::format("Device logic patch version incorrect. You have patch version {}; but at least version {} "
							"is required. Please update by following the Flashy documentation at "
							"'https://docs.inivation.com/hardware/hardware-advanced-usage/firmware-update.html'.",
					mLogicPatch, LOGIC_MINIMUM_PATCH));
		}

		// Gather device information.
		mResolution = cv::Size{static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_SIZE_COLUMNS)),
			static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_SIZE_ROWS))};

		mSensorModel = dv::IntegerAsEnum<SensorModel>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_CHIP_IDENTIFIER));

		mImuModel = dv::IntegerAsEnum<imu::ImuModel>(spiConfigReceive(MODULE_IMU, IMU_TYPE));

		mTimestampMaster = spiConfigReceive(MODULE_SYSINFO, SYSINFO_DEVICE_IS_MASTER);

		// Configure parser regarding orientation of sensors.
		const auto dvsOrientation = spiConfigReceive(MODULE_DVS, DVS_ORIENTATION_INFO);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Internal events orientation - Invert Axes: {}, Flip Horizontal: {}, Flip Vertical: {}",
				static_cast<bool>(dvsOrientation & 0x04), static_cast<bool>(dvsOrientation & 0x02),
				static_cast<bool>(dvsOrientation & 0x01)));

		const auto imuOrientation = spiConfigReceive(MODULE_IMU, IMU_ORIENTATION_INFO);

		log(LogLevel::LVL_DEBUG, fmt::format("Internal IMU orientation - Flip X: {}, Flip Y: {}, Flip Z: {}",
									 static_cast<bool>(imuOrientation & 0x04), static_cast<bool>(imuOrientation & 0x02),
									 static_cast<bool>(imuOrientation & 0x01)));

		mParser = std::make_unique<parser::DVXplorer::Parser>(mResolution, dvsOrientation, imuOrientation);

		// Gather device clock information.
		const auto logicClock = static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_CLOCK));
		const auto usbClock   = static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_USB_CLOCK));
		const auto clockDeviationFactor
			= static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_CLOCK_DEVIATION));

		// Calculate actual clock frequencies.
		mLogicClockActual = static_cast<float>(logicClock * (clockDeviationFactor / 1000.0));
		mUSBClockActual   = static_cast<float>(usbClock * (clockDeviationFactor / 1000.0));

		log(LogLevel::LVL_DEBUG, fmt::format("Device clocks - Logic: {}, USB: {}", mLogicClockActual, mUSBClockActual));

		log(LogLevel::LVL_INFO,
			fmt::format("Events: {}, IMU: {}, Master: {}", mResolution, mImuModel, mTimestampMaster));

		// Shut down device as if calling the destructor, this ensures an improperly shutdown device (program crash)
		// goes back into a known state that's not generating any data.
		setEventsRunning(false);
		setImuRunningAccelerometer(false);
		setImuRunningGyroscope(false);
		setImuRunningTemperature(false);
		setDetectorRunning(false);
		setGeneratorRunning(false);

		spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, false);
		spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, false);
		spiConfigSend(MODULE_USB, USB_RUN, false);

		spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, false); // Put DVS in reset.

		// Wait 100ms for device to clear buffers.
		std::this_thread::sleep_for(std::chrono::milliseconds(100));

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

		// Initialize and configure Samsung DVS chip.
		spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, true); // Take DVS out of reset.

		// Wait 10ms for DVS to start.
		std::this_thread::sleep_for(std::chrono::milliseconds(10));

		// Bias reset.
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_OTP_TRIM, 0x24);

		// Bias enable.
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_PINS_BUFP, 0x03);
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_PINS_BUFN, 0x7F);
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_PINS_DOB, false);

		// Initial biasing (simple default values).
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGSFONREST, 0x04);
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGALOGD_MONITOR, 0x14);
		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_LEVEL_SFOFF, 0x7D);

		spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_AMP, 4);

		setContrastThresholdOn(9);
		setContrastThresholdOff(9);

		// System settings.
		spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_CLOCK_DIVIDER_SYS, 0xA0); // Divide freq by 10.
		spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_PARALLEL_OUT_CONTROL, 0x00);
		spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_PARALLEL_OUT_ENABLE, 0x01);
		spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_PACKET_FORMAT, 0x80); // Enable MGROUP compression.

		// Digital settings.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL, 0x0C); // R/AY signals enable.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_BOOT_SEQUENCE, 0x08);

		// 1000µs = 1ms -> value is 999 (1000-1), 999 in hex is 0x03E7.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_TIMESTAMP_REFUNIT, 0x03);
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_TIMESTAMP_REFUNIT + 1, 0xE7);

		// Fine clock counts based on clock frequency.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_TIMESTAMP_SUBUNIT, (SYSTEM_CLOCK_FREQUENCY - 1));
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_DTAG_REFERENCE, SYSTEM_CLOCK_FREQUENCY);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT_FINE, SYSTEM_CLOCK_FREQUENCY);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_COUNT_FINE, SYSTEM_CLOCK_FREQUENCY);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_END_FINE, SYSTEM_CLOCK_FREQUENCY);

		// Timing settings.
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_COUNT, 0);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_COUNT + 1, 0);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_COUNT + 2, 0);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_END, 0);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_END + 1, 0);
		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GRS_END + 2, 1);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_FIRST_SELX_START, 4);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_SELX_WIDTH, 6);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_AY_START, 4);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_AY_END, 6);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_R_START, 8);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_R_END, 10);

		spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_GH_CNT, 4);

		// Disable activity decision and histogram, not currently used/mapped.
		spiConfigSend(MODULE_DEVICE, REGISTER_ACTIVITY_DECISION_BYPASS, 0x01);
		spiConfigSend(MODULE_DEVICE, REGISTER_SPATIAL_HISTOGRAM_OFF, 0x01);

		// External trigger.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_EXTERNAL_TRIGGER, 0);

		// Readout configuration.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_GLOBAL_RESET_READOUT, false);

		// SUBSAMPLE_ENABLE - AREA_BLOCK_DISABLE.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_ENABLE, 0x02);

		if (mSensorModel == SensorModel::DVX_VGA) {
			setDualBinning(false);
		}
		setGlobalReset(false);
		setGlobalHold(true);
		setFlatten(Flatten::NONE);
		setSubSampleHorizontal(SubSample::EVERY_PIXEL);
		setSubSampleVertical(SubSample::EVERY_PIXEL);
		setReadoutFPS(ReadoutFPS::VARIABLE_5000);

		setCropArea({
			{0, 0},
            mResolution
        });

		// DTAG restart after config.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_RESTART, DVS_CHIP_DTAG_CONTROL_RESTART);

		// Default configuration of IMU, External Input.
		spiConfigSend(MODULE_MULTIPLEXER, MUX_DROP_EXTINPUT_ON_TRANSFER_STALL, true);
		spiConfigSend(MODULE_MULTIPLEXER, MUX_DROP_DVS_ON_TRANSFER_STALL, false);

		setImuAccelDataRate(imu::BoschBMI160AccelDataRate::RATE_800HZ);
		setImuAccelFilter(imu::BoschBMI160AccelFilter::FILTER_NORMAL);
		setImuAccelRange(imu::BoschBMI160AccelRange::RANGE_4G);
		setImuGyroDataRate(imu::BoschBMI160GyroDataRate::RATE_800HZ);
		setImuGyroFilter(imu::BoschBMI160GyroFilter::FILTER_NORMAL);
		setImuGyroRange(imu::BoschBMI160GyroRange::RANGE_500DPS);

		setDetectorRisingEdges(false);
		setDetectorFallingEdges(false);

		setGeneratorHighTime(std::chrono::microseconds{5});
		setGeneratorLowTime(std::chrono::microseconds{5});
		setGeneratorInjectTriggerOnRisingEdge(false);
		setGeneratorInjectTriggerOnFallingEdge(false);

		setUSBEarlyPacketDelay(std::chrono::milliseconds{1});

		// USB and parser setup.
		setDataCallback(std::bind_front(&DVXplorer::usbDataCallback, this));
		setShutdownCallback(std::bind_front(&DVXplorer::shutdownCallback, this));
		// No debug callback required, log messages are handled in USBDevice class.

		mParser->setLogger([this](const bool debug, const std::string_view message) {
			log((debug) ? (LogLevel::LVL_DEBUG) : (LogLevel::LVL_ERROR), message);
		});

		mParser->setDataCommitCallback(std::bind_front(&DVXplorer::dataParserCallback, this));
		mParser->setTimeInitCallback(std::bind_front(&DVXplorer::timeInitCallback, this));

		usbDebugTransfersStart();

		usbDataTransfersStart();

		try {
			// Enable data transfer on USB end-point 2.
			spiConfigSend(MODULE_USB, USB_RUN, true);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, true);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, true);

			// Wait 10 ms for data transfer to be ready.
			std::this_thread::sleep_for(std::chrono::milliseconds(10));

			// Enable data producers.
			setEventsRunning(true);
			setImuRunningAccelerometer(true);
			setImuRunningGyroscope(true);
			setImuRunningTemperature(true);
			setDetectorRunning(true);
			setGeneratorRunning(false);

			// Timestamp reset to sync up time.
			sendTimestampReset();
			waitForTimestampReset();
		}
		catch (const dv::exceptions::RuntimeError &) {
			usbDebugTransfersStop();

			usbDataTransfersStop();

			throw;
		}
	}

	~DVXplorer() override {
		try {
			setEventsRunning(false);
			setImuRunningAccelerometer(false);
			setImuRunningGyroscope(false);
			setImuRunningTemperature(false);
			setDetectorRunning(false);
			setGeneratorRunning(false);

			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, false);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, false);
			spiConfigSend(MODULE_USB, USB_RUN, false);

			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, false); // Put DVS in reset.
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Ignore error, usually because device is gone. Always try to shut-down, as the device might not have gone
			// away fully and can still receive the command.
		}

		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		usbDebugTransfersStop();

		usbDataTransfersStop();
	}

	[[nodiscard]] std::string getCameraName() const override {
		return getDeviceName();
	}

	/**
	 * Get camera FPGA logic version.
	 * @return camera FPGA logic version
	 */
	[[nodiscard]] uint32_t getLogicVersion() const {
		return mLogicVersion;
	}

	/**
	 * Get camera FPGA logic patch level.
	 * @return camera FPGA logic patch level
	 */
	[[nodiscard]] uint32_t getLogicPatchLevel() const {
		return mLogicPatch;
	}

	[[nodiscard]] std::optional<cv::Size> getEventResolution() const override {
		return mResolution;
	}

	[[nodiscard]] std::optional<cv::Size> getFrameResolution() const override {
		return std::nullopt;
	}

	[[nodiscard]] imu::ImuModel getImuModel() const override {
		return mImuModel;
	}

	[[nodiscard]] float getPixelPitch() const override {
		if (mSensorModel == SensorModel::LITE_QVGA) {
			// Lite QVGA.
			return 18e-6f;
		}
		else {
			// S5K231Y VGA.
			return 9e-6f;
		}
	}

	[[nodiscard]] bool isMaster() const override {
		return mTimestampMaster;
	}

	[[nodiscard]] bool isEventStreamAvailable() const override {
		// All supported cameras have event output right now
		return true;
	}

	[[nodiscard]] bool isFrameStreamAvailable() const override {
		return false;
	}

	[[nodiscard]] bool isImuStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isTriggerStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isRunning() const override {
		return mIsRunning;
	}

	[[nodiscard]] bool isRunning(const std::string_view streamName) const override {
		if (streamName == "events") {
			return isRunning() && isEventsRunning();
		}
		if (streamName == "imu") {
			return isRunning() && (isImuRunningAccelerometer() || isImuRunningGyroscope() || isImuRunningTemperature());
		}
		if (streamName == "triggers") {
			return isRunning() && isDetectorRunning();
		}

		throw dv::exceptions::InvalidArgument<std::string>(
			"Stream with a given name is not available", std::string{streamName});
	}

	[[nodiscard]] bool isRunningAny() const override {
		return isRunning("events") || isRunning("imu") || isRunning("triggers");
	}

	[[nodiscard]] std::chrono::microseconds getTimeInterval() const override {
		return mParser->getTimeInterval();
	}

	void setTimeInterval(const std::chrono::microseconds timeInterval) override {
		mParser->setTimeInterval(timeInterval);
	}

	[[nodiscard]] std::chrono::microseconds getTimestampOffset() const override {
		return mParser->getSystemOffset();
	}

	/**
	 * Report if the event output is running.
	 * @return true if events active, false otherwise.
	 */
	[[nodiscard]] bool isEventsRunning() const {
		std::scoped_lock lock{mConfigLock};

		return spiConfigReceive(MODULE_DVS, DVS_RUN);
	}

	/**
	 * Enable or disable the event output.
	 * @param run whether to enable the event output or not.
	 */
	void setEventsRunning(const bool run) {
		std::scoped_lock lock{mConfigLock};

		spiConfigSend(MODULE_DVS, DVS_RUN, run);

		if (run) {
			spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_MODE, DVS_CHIP_MODE_STREAM);
		}
		else {
			spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_MODE, DVS_CHIP_MODE_OFF);
		}
	}

	[[nodiscard]] bool getFlipHorizontal() const override {
		return mParser->getDVSFlipHorizontal();
	}

	void setFlipHorizontal(const bool flipHorizontal) override {
		std::scoped_lock lock{mConfigLock};

		mParser->setDVSFlipHorizontal(flipHorizontal);

		setCropAreaInternal(mCropArea);
	}

	[[nodiscard]] bool getFlipVertical() const override {
		return mParser->getDVSFlipVertical();
	}

	void setFlipVertical(const bool flipVertical) override {
		std::scoped_lock lock{mConfigLock};

		mParser->setDVSFlipVertical(flipVertical);

		setCropAreaInternal(mCropArea);
	}

	[[nodiscard]] Flatten getFlatten() const override {
		std::scoped_lock lock{mConfigLock};

		const auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_CONTROL_PACKET_FORMAT);

		if (reg & 0x40) {
			return Flatten::FLATTEN_ON;
		}
		else if (reg & 0x20) {
			return Flatten::ON_ONLY;
		}
		else if (reg & 0x10) {
			return Flatten::OFF_ONLY;
		}
		else {
			return Flatten::NONE;
		}
	}

	void setFlatten(const Flatten flatten) override {
		std::scoped_lock lock{mConfigLock};

		auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_CONTROL_PACKET_FORMAT);

		reg &= ~0x70;

		switch (flatten) {
			case Flatten::NONE:
				break;

			case Flatten::FLATTEN_ON:
				reg |= 0x40;
				break;

			case Flatten::ON_ONLY:
				reg |= 0x20;
				break;

			case Flatten::OFF_ONLY:
				reg |= 0x10;
				break;
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_CONTROL_PACKET_FORMAT, reg);
	}

	/**
	 * Get horizontal subsampling mode.
	 * @return current horizontal subsampling mode.
	 */
	[[nodiscard]] SubSample getSubSampleHorizontal() const {
		std::scoped_lock lock{mConfigLock};

		auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO);

		reg &= 0x07;

		if (mSensorModel == SensorModel::LITE_QVGA) {
			reg >>= 1;
		}

		return dv::IntegerAsEnum<SubSample>(reg);
	}

	/**
	 * Set horizontal subsampling mode.
	 * DVXplorer Lite only supports EVERY_PIXEL, EVERY_SECOND and EVERY_FOURTH.
	 * @param subSampleHorizontal new horizontal subsampling mode.
	 */
	void setSubSampleHorizontal(const SubSample subSampleHorizontal) {
		if ((mSensorModel == SensorModel::LITE_QVGA) && (subSampleHorizontal != SubSample::EVERY_PIXEL)
			&& (subSampleHorizontal != SubSample::EVERY_SECOND) && (subSampleHorizontal != SubSample::EVERY_FOURTH)) {
			throw dv::exceptions::RuntimeError("Only no, 1/2 and 1/4 subsampling available.");
		}

		std::scoped_lock lock{mConfigLock};

		auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO);

		reg &= ~0x07;

		if (mSensorModel == SensorModel::LITE_QVGA) {
			reg |= ((dv::EnumAsInteger(subSampleHorizontal) << 1) | 0x01);
		}
		else {
			reg |= dv::EnumAsInteger(subSampleHorizontal);
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO, reg);
	}

	/**
	 * Get vertical subsampling mode.
	 * @return current vertical subsampling mode.
	 */
	[[nodiscard]] SubSample getSubSampleVertical() const {
		std::scoped_lock lock{mConfigLock};

		auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO);

		reg >>= 3;
		reg  &= 0x07;

		if (mSensorModel == SensorModel::LITE_QVGA) {
			reg >>= 1;
		}

		return dv::IntegerAsEnum<SubSample>(reg);
	}

	/**
	 * Set vertical subsampling mode.
	 * DVXplorer Lite only supports EVERY_PIXEL, EVERY_SECOND and EVERY_FOURTH.
	 * @param subSampleVertical new vertical subsampling mode.
	 */
	void setSubSampleVertical(const SubSample subSampleVertical) {
		if ((mSensorModel == SensorModel::LITE_QVGA) && (subSampleVertical != SubSample::EVERY_PIXEL)
			&& (subSampleVertical != SubSample::EVERY_SECOND) && (subSampleVertical != SubSample::EVERY_FOURTH)) {
			throw dv::exceptions::RuntimeError("Only no, 1/2 and 1/4 subsampling available.");
		}

		std::scoped_lock lock{mConfigLock};

		auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO);

		reg &= ~0x38;

		if (mSensorModel == SensorModel::LITE_QVGA) {
			reg |= (((dv::EnumAsInteger(subSampleVertical) << 1) | 0x01) << 3);
		}
		else {
			reg |= (dv::EnumAsInteger(subSampleVertical) << 3);
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_SUBSAMPLE_RATIO, reg);
	}

	/**
	 * Report status of dual-binning feature.
	 * Always false for DVXplorer Lite.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getDualBinning() const {
		if (mSensorModel == SensorModel::LITE_QVGA) {
			throw dv::exceptions::RuntimeError("Dual binning not available.");
		}

		std::scoped_lock lock{mConfigLock};

		return spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_DUAL_BINNING);
	}

	/**
	 * Enable dual-binning, maps 2x2 pixel blocks to one pixel address.
	 * Not available on DVXplorer Lite.
	 * @param dualBinning true to enable, false to disable.
	 */
	void setDualBinning(const bool dualBinning) {
		if (mSensorModel == SensorModel::LITE_QVGA) {
			throw dv::exceptions::RuntimeError("Dual binning not available.");
		}

		std::scoped_lock lock{mConfigLock};

		mParser->setDVSDualBinning(dualBinning);
		setCropAreaInternal(mCropArea);

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_DUAL_BINNING, dualBinning);
	}

	/**
	 * Report status of global hold feature.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGlobalHold() const {
		std::scoped_lock lock{mConfigLock};

		const auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL);

		return (reg & 0x01);
	}

	/**
	 * Enable or disable global hold feature. Default is enabled.
	 * For some applications like LED tracking, setting this to false can help.
	 * @param globalHold true to enable, false to disable.
	 */
	void setGlobalHold(const bool globalHold) {
		std::scoped_lock lock{mConfigLock};

		const auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL);

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL, (globalHold) ? (reg | 0x01) : (reg & ~0x01));
	}

	/**
	 * Report status of global reset feature.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGlobalReset() const {
		std::scoped_lock lock{mConfigLock};

		const auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL);

		return (reg & 0x02);
	}

	/**
	 * Enable or disable global reset feature.
	 * @param globalReset true to enable, false to disable.
	 */
	void setGlobalReset(const bool globalReset) {
		std::scoped_lock lock{mConfigLock};

		const auto reg = spiConfigReceive(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL);

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_MODE_CONTROL, (globalReset) ? (reg | 0x02) : (reg & ~0x02));
	}

	/**
	 * Get currently set event-frame readout frequency.
	 * @return event-frame readout frequency.
	 */
	[[nodiscard]] ReadoutFPS getReadoutFPS() const {
		std::scoped_lock lock{mConfigLock};

		return mReadoutFPS;
	}

	/**
	 * Set frequency of event-frame readouts on sensor.
	 * CONSTANT frequencies are guaranteed fixed frequencies with no data loss.
	 * CONSTANT_LOSSY are guaranteed fixed frequencies but will cut off a readout to respect the timing if too much data
	 * is present, resulting in data loss. VARIABLE are best-effort frequencies that will change if lots of data is
	 * present, slowing down, but without data loss.
	 * @param readoutFps event-frame readout frequency.
	 */
	void setReadoutFPS(const ReadoutFPS readoutFps) {
		std::scoped_lock lock{mConfigLock};

		mReadoutFPS = readoutFps;

		// Changes to readout timings need to happen while chip is not reading out data, to avoid lock-up.
		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_RESTART, DVS_CHIP_DTAG_CONTROL_STOP);

		if ((readoutFps == ReadoutFPS::CONSTANT_100) || (readoutFps == ReadoutFPS::CONSTANT_200)
			|| (readoutFps == ReadoutFPS::CONSTANT_500) || (readoutFps == ReadoutFPS::CONSTANT_1000)) {
			// Worst case readout time is around 0.85ms, so if we want 1k FPS or slower we need to increase T_ED
			// to compensate, and at this point it makes sense to also make the timing constant, using the
			// fixed read time readout mode. Default setting is 45000 cycles @ 50 MHz: 900ms.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL, ((900 * SYSTEM_CLOCK_FREQUENCY) >> 8));
			spiConfigSend(
				MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL + 1, ((900 * SYSTEM_CLOCK_FREQUENCY) & 0xFF));

			spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_FIXED_READ_TIME, true);

			// Safe default.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 15);
			// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 10);

			if (readoutFps == ReadoutFPS::CONSTANT_100) {
				// 10000ms per frame: 10000-900=9100
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 9);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 100);
			}
			else if (readoutFps == ReadoutFPS::CONSTANT_200) {
				// 5000ms per frame: 5000-900=4100
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 4);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 100);
			}
			else if (readoutFps == ReadoutFPS::CONSTANT_500) {
				// 2000ms per frame: 2000-900=1100
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 1);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 100);
			}
			else if (readoutFps == ReadoutFPS::CONSTANT_1000) {
				// 1000ms per frame: 1000-900=100
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 100);
			}
		}
		else if ((readoutFps == ReadoutFPS::CONSTANT_LOSSY_2000) || (readoutFps == ReadoutFPS::CONSTANT_LOSSY_5000)
				 || (readoutFps == ReadoutFPS::CONSTANT_LOSSY_10000)) {
			// Constant readout time can also be used at values below the maximum readout time, you then simply
			// hit possible data loss if the readout cannot complete in time. It's still a useful option to have.
			// Start by minimizing T_ED and readout time as much as possible, to maximize the chance of it finishing
			// in the allotted timeframe.
			spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_FIXED_READ_TIME, true);

			// 1µs T_ED, minimum safe value.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 1);

			// Smallest NEXT_SEL value.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 5);
			// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 0);

			// 1µs already consumed by T_ED above, so we limit READ to expected time - 1.
			if (readoutFps == ReadoutFPS::CONSTANT_LOSSY_2000) {
				// 500 µs per frame: 500 - 1 = 499
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL, ((499 * SYSTEM_CLOCK_FREQUENCY) >> 8));
				spiConfigSend(
					MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL + 1, ((499 * SYSTEM_CLOCK_FREQUENCY) & 0xFF));
			}
			else if (readoutFps == ReadoutFPS::CONSTANT_LOSSY_5000) {
				// 200 µs per frame: 200 - 1 = 199
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL, ((199 * SYSTEM_CLOCK_FREQUENCY) >> 8));
				spiConfigSend(
					MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL + 1, ((199 * SYSTEM_CLOCK_FREQUENCY) & 0xFF));
			}
			else if (readoutFps == ReadoutFPS::CONSTANT_LOSSY_10000) {
				// 100 µs per frame: 100 - 1 = 99
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL, ((99 * SYSTEM_CLOCK_FREQUENCY) >> 8));
				spiConfigSend(
					MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL + 1, ((99 * SYSTEM_CLOCK_FREQUENCY) & 0xFF));
			}
		}
		else {
			// For high-speed, variable time readout mode is used (less events, more speed).
			spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_FIXED_READ_TIME, false);

			spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL, ((900 * SYSTEM_CLOCK_FREQUENCY) >> 8));
			spiConfigSend(
				MODULE_DEVICE, REGISTER_TIMING_READ_TIME_INTERVAL + 1, ((900 * SYSTEM_CLOCK_FREQUENCY) & 0xFF));

			if (readoutFps == ReadoutFPS::VARIABLE_2000) {
				// 307+192=499µs min readout time. 307µs T_ED.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, (307 >> 8));
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, (307 & 0xFF));

				// Safe default.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 15);
				// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 10);
			}
			else if (readoutFps == ReadoutFPS::VARIABLE_5000) {
				// 7+192=199µs min readout time. 7µs T_ED.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 7);

				// Safe default.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 15);
				// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 10);
			}
			else if (readoutFps == ReadoutFPS::VARIABLE_10000) {
				// 10+89=99µs min readout time. 10µs T_ED.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 10);

				// Second smallest NEXT_SEL value.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 7);
				// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 2);
			}
			else if (readoutFps == ReadoutFPS::VARIABLE_15000) {
				// 1+64=65µs min readout time. 1µs T_ED, minimum safe value.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 1, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_GH_COUNT + 2, 1);

				// Smallest NEXT_SEL value.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START, 0);
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_NEXT_SELX_START + 1, 5);
				// Also set MAX_EVENT_NUM, which is defined as NEXT_SEL-5, up to a maximum of 60.
				spiConfigSend(MODULE_DEVICE, REGISTER_TIMING_MAX_EVENT_NUM, 0);
			}
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_DIGITAL_RESTART, DVS_CHIP_DTAG_CONTROL_START);
	}

	/**
	 * Get the contrast threshold for ON polarity event generation.
	 * @return ON contrast threshold, from 0 to 17.
	 */
	[[nodiscard]] uint8_t getContrastThresholdOn() const {
		std::scoped_lock lock{mConfigLock};

		return mContrastThresholdOn;
	}

	/**
	 * Set the contrast threshold for ON polarity event generation.
	 * Valid values from 0 to 17.
	 * @param contrastThresholdOn ON contrast threshold, from 0 to 17.
	 */
	void setContrastThresholdOn(const uint8_t contrastThresholdOn) {
		if (contrastThresholdOn >= 18) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Contrast threshold value must be between 0 and 17.");
		}

		std::scoped_lock lock{mConfigLock};

		mContrastThresholdOn = contrastThresholdOn;

		// ON BIAS.
		if (contrastThresholdOn < 9) {
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_ON, contrastThresholdOn);
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGSFONREST, 0x04 | 0x00);
		}
		else {
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_ON, contrastThresholdOn - 9);
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGSFONREST, 0x04 | 0x02);
		}
	}

	/**
	 * Get the contrast threshold for OFF polarity event generation.
	 * @return OFF contrast threshold, from 0 to 17.
	 */
	[[nodiscard]] uint8_t getContrastThresholdOff() const {
		std::scoped_lock lock{mConfigLock};

		return mContrastThresholdOff;
	}

	/**
	 * Set the contrast threshold for OFF polarity event generation.
	 * Valid values from 0 to 17.
	 * @param contrastThresholdOff OFF contrast threshold, from 0 to 17.
	 */
	void setContrastThresholdOff(const uint8_t contrastThresholdOff) {
		if (contrastThresholdOff >= 18) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Contrast threshold value must be between 0 and 17.");
		}

		std::scoped_lock lock{mConfigLock};

		mContrastThresholdOff = contrastThresholdOff;

		// OFF BIAS.
		if (contrastThresholdOff < 9) {
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_OFF, 8 - contrastThresholdOff);
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_LEVEL_SFOFF, 0x7D | 0x02);
		}
		else {
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_OFF, 8 - (contrastThresholdOff - 9));
			spiConfigSend(MODULE_DEVICE, REGISTER_BIAS_CURRENT_LEVEL_SFOFF, 0x7D | 0x00);
		}
	}

	[[nodiscard]] cv::Rect getCropArea() const override {
		std::scoped_lock lock{mConfigLock};

		return mCropArea;
	}

	void setCropArea(const cv::Rect cropArea) override {
		if ((cropArea.x < 0) || (cropArea.x >= mResolution.width) || (cropArea.width < 1)
			|| ((cropArea.x + cropArea.width - 1) >= mResolution.width) || (cropArea.y < 0)
			|| (cropArea.y >= mResolution.height) || (cropArea.height < 1)
			|| ((cropArea.y + cropArea.height - 1) >= mResolution.height)) {
			throw dv::exceptions::InvalidArgument<cv::Rect>("The crop area is invalid.");
		}

		std::scoped_lock lock{mConfigLock};

		mCropArea = cropArea;

		setCropAreaInternal(mCropArea);
	}

	/**
	 * Status of IMU accelerometer.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningAccelerometer() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_ACCELEROMETER);
	}

	/**
	 * Enable or disable IMU accelerometer.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningAccelerometer(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_ACCELEROMETER, run);
	}

	/**
	 * Status of IMU gyroscope.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningGyroscope() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_GYROSCOPE);
	}

	/**
	 * Enable or disable IMU gyroscope.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningGyroscope(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_GYROSCOPE, run);
	}

	/**
	 * Status of IMU temperature measurement.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningTemperature() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_TEMPERATURE);
	}

	/**
	 * Enable or disable IMU temperature measurement.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningTemperature(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_TEMPERATURE, run);
	}

	/**
	 * Status of IMU X axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipX() const {
		return mParser->getIMUFlipX();
	}

	/**
	 * Enable or disable IMU X axis flipping.
	 * Will negate (flip) all returned X axis values.
	 * @param flipX true to enable, false to disable.
	 */
	void setIMUFlipX(const bool flipX) {
		mParser->setIMUFlipX(flipX);
	}

	/**
	 * Status of IMU Y axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipY() const {
		return mParser->getIMUFlipY();
	}

	/**
	 * Enable or disable IMU Y axis flipping.
	 * Will negate (flip) all returned Y axis values.
	 * @param flipY true to enable, false to disable.
	 */
	void setIMUFlipY(const bool flipY) {
		mParser->setIMUFlipY(flipY);
	}

	/**
	 * Status of IMU Z axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipZ() const {
		return mParser->getIMUFlipZ();
	}

	/**
	 * Enable or disable IMU Z axis flipping.
	 * Will negate (flip) all returned Z axis values.
	 * @param flipZ true to enable, false to disable.
	 */
	void setIMUFlipZ(const bool flipZ) {
		mParser->setIMUFlipZ(flipZ);
	}

	/**
	 * Get current IMU accelerometer data rate.
	 * @return accelerometer data rate.
	 */
	[[nodiscard]] imu::BoschBMI160AccelDataRate getImuAccelDataRate() const {
		return dv::IntegerAsEnum<imu::BoschBMI160AccelDataRate>(spiConfigReceive(MODULE_IMU, IMU_ACCEL_DATA_RATE) + 5);
	}

	/**
	 * Set IMU accelerometer data rate.
	 * @param dataRate accelerometer data rate.
	 */
	void setImuAccelDataRate(const imu::BoschBMI160AccelDataRate dataRate) {
		spiConfigSend(MODULE_IMU, IMU_ACCEL_DATA_RATE, dv::EnumAsInteger(dataRate) - 5);
	}

	/**
	 * Get current IMU accelerometer filter setting.
	 * @return accelerometer filter setting.
	 */
	[[nodiscard]] imu::BoschBMI160AccelFilter getImuAccelFilter() const {
		return dv::IntegerAsEnum<imu::BoschBMI160AccelFilter>(spiConfigReceive(MODULE_IMU, IMU_ACCEL_FILTER));
	}

	/**
	 * Set IMU accelerometer filter setting.
	 * @param filter accelerometer filter setting.
	 */
	void setImuAccelFilter(const imu::BoschBMI160AccelFilter filter) {
		spiConfigSend(MODULE_IMU, IMU_ACCEL_FILTER, dv::EnumAsInteger(filter));
	}

	/**
	 * Get current IMU accelerometer range.
	 * @return accelerometer range.
	 */
	[[nodiscard]] imu::BoschBMI160AccelRange getImuAccelRange() const {
		const auto rangeInt = spiConfigReceive(MODULE_IMU, IMU_ACCEL_RANGE);

		switch (rangeInt) {
			case 0:
				return imu::BoschBMI160AccelRange::RANGE_2G;

			case 1:
				return imu::BoschBMI160AccelRange::RANGE_4G;

			case 2:
				return imu::BoschBMI160AccelRange::RANGE_8G;

			case 3:
				return imu::BoschBMI160AccelRange::RANGE_16G;

			default:
				throw dv::exceptions::RuntimeError("Invalid IMU accel range returned.");
		}
	}

	/**
	 * Set IMU accelerometer range.
	 * @param range accelerometer range.
	 */
	void setImuAccelRange(const imu::BoschBMI160AccelRange range) {
		switch (range) {
			case imu::BoschBMI160AccelRange::RANGE_2G:
				spiConfigSend(MODULE_IMU, IMU_ACCEL_RANGE, 0);
				break;

			case imu::BoschBMI160AccelRange::RANGE_4G:
				spiConfigSend(MODULE_IMU, IMU_ACCEL_RANGE, 1);
				break;

			case imu::BoschBMI160AccelRange::RANGE_8G:
				spiConfigSend(MODULE_IMU, IMU_ACCEL_RANGE, 2);
				break;

			case imu::BoschBMI160AccelRange::RANGE_16G:
				spiConfigSend(MODULE_IMU, IMU_ACCEL_RANGE, 3);
				break;
		}
	}

	/**
	 * Get current IMU gyroscope data rate.
	 * @return gyroscope data rate.
	 */
	[[nodiscard]] imu::BoschBMI160GyroDataRate getImuGyroDataRate() const {
		return dv::IntegerAsEnum<imu::BoschBMI160GyroDataRate>(spiConfigReceive(MODULE_IMU, IMU_GYRO_DATA_RATE) + 6);
	}

	/**
	 * Set IMU gyroscope data rate.
	 * @param dataRate gyroscope data rate.
	 */
	void setImuGyroDataRate(const imu::BoschBMI160GyroDataRate dataRate) {
		spiConfigSend(MODULE_IMU, IMU_GYRO_DATA_RATE, dv::EnumAsInteger(dataRate) - 6);
	}

	/**
	 * Get current IMU gyroscope filter setting.
	 * @return gyroscope filter setting.
	 */
	[[nodiscard]] imu::BoschBMI160GyroFilter getImuGyroFilter() const {
		return dv::IntegerAsEnum<imu::BoschBMI160GyroFilter>(spiConfigReceive(MODULE_IMU, IMU_GYRO_FILTER));
	}

	/**
	 * Set IMU gyroscope filter setting.
	 * @param filter gyroscope filter setting.
	 */
	void setImuGyroFilter(const imu::BoschBMI160GyroFilter filter) {
		spiConfigSend(MODULE_IMU, IMU_GYRO_FILTER, dv::EnumAsInteger(filter));
	}

	/**
	 * Get current IMU gyroscope range.
	 * @return gyroscope range.
	 */
	[[nodiscard]] imu::BoschBMI160GyroRange getImuGyroRange() const {
		return dv::IntegerAsEnum<imu::BoschBMI160GyroRange>(spiConfigReceive(MODULE_IMU, IMU_GYRO_RANGE));
	}

	/**
	 * Set IMU gyroscope range.
	 * @param range gyroscope range.
	 */
	void setImuGyroRange(const imu::BoschBMI160GyroRange range) {
		spiConfigSend(MODULE_IMU, IMU_GYRO_RANGE, dv::EnumAsInteger(range));
	}

	/**
	 * Report status of external signal detector.
	 * @return true if running, false otherwise.
	 */
	[[nodiscard]] bool isDetectorRunning() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_DETECTOR);
	}

	/**
	 * Enable or disable external signal detector.
	 * @param run true to enable, false to disable.
	 */
	void setDetectorRunning(const bool run) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_DETECTOR, run);
	}

	/**
	 * Report status of rising edge detection on the SIGNAL_IN line.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getDetectorRisingEdges() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_RISING_EDGES);
	}

	/**
	 * Detect rising edges (low to high transitions) on the SIGNAL_IN line.
	 * @param detectRising true to enable, false to disable.
	 */
	void setDetectorRisingEdges(const bool detectRising) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_RISING_EDGES, detectRising);
	}

	/**
	 * Report status of falling edge detection on the SIGNAL_IN line.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getDetectorFallingEdges() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_FALLING_EDGES);
	}

	/**
	 * Detect falling edges (high to low transitions) on the SIGNAL_IN line.
	 * @param detectFalling true to enable, false to disable.
	 */
	void setDetectorFallingEdges(const bool detectFalling) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_FALLING_EDGES, detectFalling);
	}

	/**
	 * Report status of external signal generator.
	 * @return true if running, false otherwise.
	 */
	[[nodiscard]] bool isGeneratorRunning() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_GENERATOR);
	}

	/**
	 * Enable or disable external signal generator (PWM-like output).
	 * @param run true to enable, false to disable.
	 */
	void setGeneratorRunning(const bool run) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_GENERATOR, run);
	}

	/**
	 * Get current PWM low time.
	 * @return low time in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getGeneratorLowTime() const {
		std::scoped_lock lock{mConfigLock};

		return getGeneratorLowTimeInternal() - getGeneratorHighTimeInternal();
	}

	/**
	 * Set PWM low time for external signal generator.
	 * @param lowTimeUs low time in microseconds.
	 */
	void setGeneratorLowTime(const std::chrono::microseconds lowTimeUs) {
		if ((lowTimeUs.count() < 1) || (lowTimeUs.count() > EXT_INPUT_TIME_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The generator low time duration is invalid.", lowTimeUs);
		}

		std::scoped_lock lock{mConfigLock};

		const auto intervalTimeUs = lowTimeUs + getGeneratorHighTimeInternal();

		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto intervalInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(intervalTimeUs.count()) * mLogicClockActual));

		intervalInTicks
			= std::clamp<uint32_t>(intervalInTicks, 0, static_cast<uint32_t>(EXT_INPUT_TIME_MAX * mLogicClockActual));

		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_INTERVAL, intervalInTicks);
	}

	/**
	 * Get current PWM high time.
	 * @return high time in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getGeneratorHighTime() const {
		std::scoped_lock lock{mConfigLock};

		return getGeneratorHighTimeInternal();
	}

	/**
	 * Set PWM high time for external signal generator.
	 * @param highTimeUs high time in microseconds.
	 */
	void setGeneratorHighTime(const std::chrono::microseconds highTimeUs) {
		if ((highTimeUs.count() < 1) || (highTimeUs.count() > EXT_INPUT_TIME_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The generator low time duration is invalid.", highTimeUs);
		}

		std::scoped_lock lock{mConfigLock};

		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto lengthInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(highTimeUs.count()) * mLogicClockActual));

		lengthInTicks
			= std::clamp<uint32_t>(lengthInTicks, 0, static_cast<uint32_t>(EXT_INPUT_TIME_MAX * mLogicClockActual));

		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_LENGTH, lengthInTicks);
	}

	/**
	 * Report status of trigger event injection feature for external signal generator rising edges.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGeneratorInjectTriggerOnRisingEdge() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE);
	}

	/**
	 * Inject a trigger event of type EXTERNAL_GENERATOR_RISING_EDGE into the event stream from the device,
	 * every time a rising edge is generated by the PWM-like output of the external signal generator.
	 *
	 * @param injectRising true to inject trigger event, false to disable.
	 */
	void setGeneratorInjectTriggerOnRisingEdge(const bool injectRising) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE, injectRising);
	}

	/**
	 * Report status of trigger event injection feature for external signal generator falling edges.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGeneratorInjectTriggerOnFallingEdge() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE);
	}

	/**
	 * Inject a trigger event of type EXTERNAL_GENERATOR_FALLING_EDGE into the event stream from the device,
	 * every time a falling edge is generated by the PWM-like output of the external signal generator.
	 *
	 * @param injectFalling true to inject trigger event, false to disable.
	 */
	void setGeneratorInjectTriggerOnFallingEdge(const bool injectFalling) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE, injectFalling);
	}

	/**
	 * Get value of USB early packet timeout.
	 * @return timeout in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getUSBEarlyPacketDelay() const {
		// Times are in µs on host, but in cycles @ USB_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto delayInTicks = spiConfigReceive(MODULE_USB, USB_EARLY_PACKET_DELAY);

		return std::chrono::microseconds{std::llround(static_cast<float>(delayInTicks) / mUSBClockActual)};
	}

	/**
	 * Send data over USB early if this timeout is reached, instead of waiting on buffers being full.
	 * Timeout on device is in 125µs time-slices.
	 * @param earlyPacketDelayUs timeout in microseconds.
	 */
	void setUSBEarlyPacketDelay(const std::chrono::microseconds earlyPacketDelayUs) {
		if ((earlyPacketDelayUs.count() < 125) || (earlyPacketDelayUs.count() > USB_EARLY_PACKET_DELAY_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The USB Early Packet delay is invalid.", earlyPacketDelayUs);
		}

		// Times are in µs on host, but in cycles @ USB_CLOCK_FREQ on FPGA, so we adjust for that.
		auto delayInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(earlyPacketDelayUs.count()) * mUSBClockActual));

		delayInTicks = std::clamp<uint32_t>(
			delayInTicks, 0, static_cast<uint32_t>(USB_EARLY_PACKET_DELAY_MAX * mUSBClockActual));

		spiConfigSend(MODULE_USB, USB_EARLY_PACKET_DELAY, delayInTicks);
	}

protected:
	void sendTimestampReset() override {
		mSyncState = SyncState::WAIT_RESET;

		std::array<SPIConfigurationParameters, 2> tsReset{};
		tsReset[0].moduleAddress    = MODULE_MULTIPLEXER;
		tsReset[0].parameterAddress = MUX_TIMESTAMP_RESET;
		tsReset[0].parameterValue   = true;
		tsReset[1].moduleAddress    = MODULE_MULTIPLEXER;
		tsReset[1].parameterAddress = MUX_TIMESTAMP_RESET;
		tsReset[1].parameterValue   = false;

		spiConfigSendMultiple(tsReset);
	}

	void setTimestampOffset(const std::chrono::microseconds timestampOffset) override {
		mParser->setSystemOffset(timestampOffset);
		mBuffers.clearBuffers();
	}

	[[nodiscard]] std::chrono::microseconds getGeneratorHighTimeInternal() const {
		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto lengthInTicks = spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_LENGTH);

		return std::chrono::microseconds{std::llround(static_cast<float>(lengthInTicks) / mLogicClockActual)};
	}

	[[nodiscard]] std::chrono::microseconds getGeneratorLowTimeInternal() const {
		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto intervalInTicks = spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_INTERVAL);

		const auto intervalTimeUs
			= std::chrono::microseconds{std::llround(static_cast<float>(intervalInTicks) / mLogicClockActual)};

		return intervalTimeUs - getGeneratorHighTime();
	}

	void setCropAreaInternal(const cv::Rect cropArea) {
		// Disable cropper on chip while changing configuration.
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_BYPASS, true);

		if ((cropArea.x == 0) && (cropArea.y == 0) && (cropArea.width == mResolution.width)
			&& (cropArea.height == mResolution.height)) {
			// ROI equals full size, so no actual ROI.
			return;
		}

		uint16_t startX = cropArea.x;
		uint16_t startY = cropArea.y;
		uint16_t endX   = cropArea.x + cropArea.width - 1;
		uint16_t endY   = cropArea.y + cropArea.height - 1;

		if (mParser->getDVSDualBinning()) {
			startX /= 2;
			startY /= 2;
			endX   /= 2;
			endY   /= 2;
		}

		// Negate due to internal flip.
		if (!getFlipHorizontal()) {
			uint16_t width = mResolution.width;
			if (mParser->getDVSDualBinning()) {
				width /= 2;
			}

			startX = width - 1 - startX;
			endX   = width - 1 - endX;

			std::swap(startX, endX);
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_X_START_ADDRESS, (startX >> 8));
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_X_START_ADDRESS + 1, (startX & 0xFF));

		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_X_END_ADDRESS, (endX >> 8));
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_X_END_ADDRESS + 1, (endX & 0xFF));

		// Negate due to internal flip.
		if (!getFlipVertical()) {
			uint16_t height = mResolution.height;
			if (mParser->getDVSDualBinning()) {
				height /= 2;
			}

			startY = height - 1 - startY;
			endY   = height - 1 - endY;

			std::swap(startY, endY);
		}

		// Cropper has a special corner case:
		// if both start and end pixels are in the same group,
		// only the mask in the END register is actually applied.
		// We must track the addresses, detect this, and properly
		// update all the masks as needed.
		const uint8_t startGroup = (startY / 8);
		uint8_t startMask        = (0x00FF << (startY % 8));

		const uint8_t endGroup = (endY / 8);
		uint8_t endMask        = (0x00FF >> (7 - (endY % 8)));

		if (startGroup == endGroup) {
			// EndMask must let pass only the unblocked pixels (set to 1).
			endMask = startMask & endMask;

			// StartMask doesn't matter in this case, reset to 0xFF.
			startMask = 0xFF;
		}

		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_Y_START_GROUP, startGroup);
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_Y_START_MASK, startMask);

		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_Y_END_GROUP, endGroup);
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_Y_END_MASK, endMask);

		// Enable cropper on chip (ROI functionality).
		spiConfigSend(MODULE_DEVICE, REGISTER_CROPPER_BYPASS, false);
	}

private:
	enum class SensorModel {
		LITE_QVGA = 19,
		DVX_VGA   = 20,
	};

	/**
	 * Module address: device-side Multiplexer configuration.
	 * The Multiplexer is responsible for mixing, timestamping and outputting
	 * (via USB) the various event types generated by the device. It is also
	 * responsible for timestamp generation and synchronization.
	 */
	static constexpr uint16_t MODULE_MULTIPLEXER{0};
	/**
	 * Module address: device-side DVS configuration.
	 * The DVS state machine interacts with the DVS chip and gets the
	 * polarity events from it. It supports various configurable delays, as
	 * well as advanced filtering capabilities on the polarity events.
	 */
	static constexpr uint16_t MODULE_DVS{1};
	/**
	 * Module address: device-side IMU (Inertial Measurement Unit) configuration.
	 * The IMU module connects to the external IMU chip and sends data on the
	 * device's movement in space. It can configure various options on the external
	 * chip, such as accelerometer range or gyroscope refresh rate.
	 */
	static constexpr uint16_t MODULE_IMU{3};
	/**
	 * Module address: device-side External Input (signal detector/generator) configuration.
	 * The External Input module is used to detect external signals on the external input
	 * jack and inject an event into the event stream when this happens. It can detect pulses
	 * of a specific length or rising and falling edges.
	 * On some systems, a signal generator module is also present, which can generate
	 * PWM-like pulsed signals with configurable timing.
	 */
	static constexpr uint16_t MODULE_EXTERNAL_INPUT{4};

	/**
	 * Module address: device-side system information.
	 * The system information module provides various details on the device, such
	 * as currently installed logic revision or clock speeds.
	 * All its parameters are read-only.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_info'
	 * documentation for more details on what information is available.
	 */
	static constexpr uint16_t MODULE_SYSINFO{6};
	/**
	 * Module address: device-side USB output configuration.
	 * The USB output module forwards the data from the device and the FPGA/CPLD to
	 * the USB chip, usually a Cypress FX2 or FX3.
	 */
	static constexpr uint16_t MODULE_USB{9};

	/**
	 * Parameter address for module MUX:
	 * run the Multiplexer state machine, which is responsible for
	 * mixing the various event types at the device level, timestamping
	 * them and outputting them via USB or other connectors.
	 */
	static constexpr uint16_t MUX_RUN{0};
	/**
	 * Parameter address for module MUX:
	 * run the Timestamp Generator inside the Multiplexer state machine,
	 * which will provide microsecond accurate timestamps to the
	 * events passing through.
	 */
	static constexpr uint16_t MUX_TIMESTAMP_RUN{1};
	/**
	 * Parameter address for module MUX:
	 * reset the Timestamp Generator to zero. This also sends a reset
	 * pulse to all connected slave devices, resetting their timestamp too.
	 */
	static constexpr uint16_t MUX_TIMESTAMP_RESET{2};
	/**
	 * Parameter address for module MUX:
	 * power up the chip's bias generator, enabling the chip to work.
	 */
	static constexpr uint16_t MUX_RUN_CHIP{3};
	/**
	 * Parameter address for module MUX:
	 * drop External Input events if the USB output FIFO is full, instead of having
	 * them pile up at the input FIFOs.
	 */
	static constexpr uint16_t MUX_DROP_EXTINPUT_ON_TRANSFER_STALL{4};
	/**
	 * Parameter address for module MUX:
	 * drop DVS events if the USB output FIFO is full, instead of having
	 * them pile up at the input FIFOs.
	 */
	static constexpr uint16_t MUX_DROP_DVS_ON_TRANSFER_STALL{5};
	/**
	 * Parameter address for module MUX:
	 * read-only parameter, representing the number of dropped
	 * External Input events on the device due to full USB buffers.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t MUX_STATISTICS_EXTINPUT_DROPPED{81};
	/**
	 * Parameter address for module MUX:
	 * read-only parameter, representing the number of dropped
	 * DVS events on the device due to full USB buffers.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t MUX_STATISTICS_DVS_DROPPED{83};

	/**
	 * Parameter address for module DVS:
	 * read-only parameter, contains the X axis resolution of the
	 * DVS events returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_SIZE_COLUMNS{0};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, contains the Y axis resolution of the
	 * DVS events returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_SIZE_ROWS{1};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, contains information on the orientation
	 * of the X/Y axes, whether they should be inverted or not on
	 * the host when parsing incoming events.
	 * Bit 2: dvsInvertXY
	 * Bit 1: reserved
	 * Bit 0: reserved
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_ORIENTATION_INFO{2};
	/**
	 * Parameter address for module DVS:
	 * run the DVS state machine and read out polarity events from the chip.
	 */
	static constexpr uint16_t DVS_RUN{3};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, representing the number of column
	 * transactions completed successfully on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_COLUMN{81};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, representing the number of SGroup/MGroup
	 * transactions completed successfully on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_GROUP{83};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, representing the number of dropped
	 * column transactions on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_DROPPED_COLUMN{85};
	/**
	 * Parameter address for module DVS:
	 * read-only parameter, representing the number of dropped
	 * SGroup/MGroup transactions on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_DROPPED_GROUP{87};

	/**
	 * Parameter address for module IMU:
	 * read-only parameter, contains information on the type of IMU
	 * chip being used in this device:
	 * 0 - no IMU present
	 * 3 - Bosch BMI 160
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t IMU_TYPE{0};
	/**
	 * Parameter address for module IMU:
	 * read-only parameter, contains information on the orientation
	 * of the X/Y/Z axes, whether they should be flipped or not on
	 * the host when parsing incoming IMU data samples.
	 * Bit 2: imuFlipX
	 * Bit 1: imuFlipY
	 * Bit 0: imuFlipZ
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Generated IMU events are already
	 * properly flipped when returned to the user.
	 */
	static constexpr uint16_t IMU_ORIENTATION_INFO{1};
	/**
	 * Parameter address for module IMU:
	 * enable the IMU's accelerometer. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_ACCELEROMETER{2};
	/**
	 * Parameter address for module IMU:
	 * enable the IMU's gyroscope. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_GYROSCOPE{3};
	/**
	 * Parameter address for module IMU:
	 * enable the IMU's temperature sensor. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_TEMPERATURE{4};
	/**
	 * Parameter address for module IMU:
	 * 8 settings:
	 * 0 - 12.5 Hz
	 * 1 - 25 Hz
	 * 2 - 50 Hz
	 * 3 - 100 Hz
	 * 4 - 200 Hz
	 * 5 - 400 Hz
	 * 6 - 800 Hz
	 * 7 - 1600 Hz
	 */
	static constexpr uint16_t IMU_ACCEL_DATA_RATE{5};
	/**
	 * Parameter address for module IMU:
	 * 3 settings:
	 * 0 - OSR4
	 * 1 - OSR2
	 * 2 - Normal
	 */
	static constexpr uint16_t IMU_ACCEL_FILTER{6};
	/**
	 * Parameter address for module IMU:
	 * 4 settings:
	 * 0 - +- 2g
	 * 1 - +- 4g
	 * 2 - +- 8g
	 * 3 - +- 16g
	 */
	static constexpr uint16_t IMU_ACCEL_RANGE{7};
	/**
	 * Parameter address for module IMU:
	 * 8 settings:
	 * 0 - 25 Hz
	 * 1 - 50 Hz
	 * 2 - 100 Hz
	 * 3 - 200 Hz
	 * 4 - 400 Hz
	 * 5 - 800 Hz
	 * 6 - 1600 Hz
	 * 7 - 3200 Hz
	 */
	static constexpr uint16_t IMU_GYRO_DATA_RATE{8};
	/**
	 * Parameter address for module IMU:
	 * 3 settings:
	 * 0 - OSR4
	 * 1 - OSR2
	 * 2 - Normal
	 */
	static constexpr uint16_t IMU_GYRO_FILTER{9};
	/**
	 * Parameter address for module IMU:
	 * 5 settings:
	 * 0 - +- 2000°/s
	 * 1 - +- 1000°/s
	 * 2 - +- 500°/s
	 * 3 - +- 250°/s
	 * 4 - +- 125°/s
	 */
	static constexpr uint16_t IMU_GYRO_RANGE{10};

	/**
	 * Parameter address for module EXTINPUT:
	 * enable the signal detector module. It generates events
	 * when it sees certain types of signals, such as edges or
	 * pulses of a defined length, on the SIGNAL pin of the
	 * INPUT synchronization connector.
	 * This can be useful to inject events into the event
	 * stream in response to external stimuli or controls,
	 * such as turning on a LED lamp.
	 */
	static constexpr uint16_t EXTINPUT_RUN_DETECTOR{0};
	/**
	 * Parameter address for module EXTINPUT:
	 * send a special EXTERNAL_INPUT_RISING_EDGE event when a
	 * rising edge is detected (transition from low voltage to high).
	 */
	static constexpr uint16_t EXTINPUT_DETECT_RISING_EDGES{1};
	/**
	 * Parameter address for module EXTINPUT:
	 * send a special EXTERNAL_INPUT_FALLING_EDGE event when a
	 * falling edge is detected (transition from high voltage to low).
	 */
	static constexpr uint16_t EXTINPUT_DETECT_FALLING_EDGES{2};

	/**
	 * Parameter address for module EXTINPUT:
	 * enable the signal generator module. It generates a
	 * PWM-like signal based on configurable parameters and
	 * outputs it on the OUT JACK signal.
	 */
	static constexpr uint16_t EXTINPUT_RUN_GENERATOR{11};
	/**
	 * Parameter address for module EXTINPUT:
	 * the interval between the start of two consecutive pulses,
	 * expressed in cycles at LogicClock frequency (see
	 * 'struct caer_davis_info' for details on how to get the frequency).
	 * This must be bigger or equal to EXTINPUT_GENERATE_PULSE_LENGTH.
	 * To generate a signal with 50% duty cycle, this would
	 * have to be exactly double of EXTINPUT_GENERATE_PULSE_LENGTH.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_PULSE_INTERVAL{13};
	/**
	 * Parameter address for module EXTINPUT:
	 * the length a pulse stays active, expressed in cycles at
	 * LogicClock frequency (see 'struct caer_davis_info' for
	 * details on how to get the frequency). This must be
	 * smaller or equal to EXTINPUT_GENERATE_PULSE_INTERVAL.
	 * To generate a signal with 50% duty cycle, this would
	 * have to be exactly half of EXTINPUT_GENERATE_PULSE_INTERVAL.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_PULSE_LENGTH{14};
	/**
	 * Parameter address for module EXTINPUT:
	 * enables event injection when a rising edge occurs in the
	 * generated signal; a special event EXTERNAL_GENERATOR_RISING_EDGE
	 * is emitted into the event stream.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE{15};
	/**
	 * Parameter address for module EXTINPUT:
	 * enables event injection when a falling edge occurs in the
	 * generated signal; a special event EXTERNAL_GENERATOR_FALLING_EDGE
	 * is emitted into the event stream.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE{16};

	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, the version of the logic currently
	 * running on the device's FPGA/CPLD.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_VERSION{0};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, an integer used to identify the different
	 * types of sensor chips used on the device.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_CHIP_IDENTIFIER{1};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, whether the device is currently a timestamp
	 * master or slave when synchronizing multiple devices together.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_DEVICE_IS_MASTER{2};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, the frequency in MHz at which the main
	 * FPGA/CPLD logic is running.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_CLOCK{3};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, the frequency in MHz at which the FPGA/CPLD
	 * logic related to USB data transmission is running.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_USB_CLOCK{5};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, the deviation factor for the clocks.
	 * Due to how FX3 generates the clocks, which are then used by
	 * FPGA/CPLD, they are not integers but have a fractional part.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_CLOCK_DEVIATION{6};
	/**
	 * Parameter address for module SYSINFO:
	 * read-only parameter, the patch version of the logic currently
	 * running on the device's FPGA/CPLD.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_PATCH{7};

	/**
	 * Parameter address for module USB:
	 * enable the USB FIFO module, which transfers the data from the
	 * FPGA/CPLD to the USB chip, to be then sent to the host.
	 * Turning this off will suppress any USB data communication!
	 */
	static constexpr uint16_t USB_RUN{0};
	/**
	 * Parameter address for module USB:
	 * the time delay after which a packet of data is committed to
	 * USB, even if it is not full yet (short USB packet).
	 * The value is in 125µs time-slices, corresponding to how
	 * USB schedules its operations (a value of 4 for example
	 * would mean waiting at most 0.5ms until sending a short
	 * USB packet to the host).
	 */
	static constexpr uint16_t USB_EARLY_PACKET_DELAY{1};

	/**
	 * Module address: device-side chip configuration.
	 * This state machine is responsible for configuring the Samsung DVS chip.
	 */
	static constexpr uint16_t MODULE_DEVICE{5};

	static constexpr uint16_t REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGSFONREST{0x000B};
	static constexpr uint16_t REGISTER_BIAS_CURRENT_RANGE_SELECT_LOGALOGD_MONITOR{0x000C};
	static constexpr uint16_t REGISTER_BIAS_OTP_TRIM{0x000D};
	static constexpr uint16_t REGISTER_BIAS_PINS_DBGP{0x000F};
	static constexpr uint16_t REGISTER_BIAS_PINS_DBGN{0x0010};
	static constexpr uint16_t REGISTER_BIAS_CURRENT_LEVEL_SFOFF{0x0012};
	static constexpr uint16_t REGISTER_BIAS_PINS_BUFP{0x0013};
	static constexpr uint16_t REGISTER_BIAS_PINS_BUFN{0x0014};
	static constexpr uint16_t REGISTER_BIAS_PINS_DOB{0x0015};
	static constexpr uint16_t REGISTER_BIAS_CURRENT_AMP{0x0018};
	static constexpr uint16_t REGISTER_BIAS_CURRENT_ON{0x001C};
	static constexpr uint16_t REGISTER_BIAS_CURRENT_OFF{0x001E};

	static constexpr uint16_t REGISTER_CONTROL_MODE{0x3000};
	static constexpr uint16_t REGISTER_CONTROL_INTERRUPT_SOURCE{0x3004};
	static constexpr uint16_t REGISTER_CONTROL_INTERRUPT_ENABLE_TIME{0x3005};
	static constexpr uint16_t REGISTER_CONTROL_INTERRUPT_ACKNOWLEDGE{0x3007};
	static constexpr uint16_t REGISTER_CONTROL_INTERRUPT_AUTO_MODE{0x3008};
	static constexpr uint16_t REGISTER_CONTROL_INTERRUPT_RELEASE_TIME{0x3009};

	static constexpr uint16_t REGISTER_CONTROL_PLL_P{0x300D};
	static constexpr uint16_t REGISTER_CONTROL_PLL_M{0x300E};
	static constexpr uint16_t REGISTER_CONTROL_PLL_S{0x3010};
	static constexpr uint16_t REGISTER_CONTROL_CLOCK_DIVIDER_SYS{0x3011};
	static constexpr uint16_t REGISTER_CONTROL_CLOCK_DIVIDER_PVI{0x3012};
	static constexpr uint16_t REGISTER_CONTROL_PARALLEL_OUT_CONTROL{0x3019};
	static constexpr uint16_t REGISTER_CONTROL_PARALLEL_OUT_ENABLE{0x301E};
	static constexpr uint16_t REGISTER_CONTROL_PACKET_FORMAT{0x3067};

	static constexpr uint16_t REGISTER_DIGITAL_ENABLE{0x3200};
	static constexpr uint16_t REGISTER_DIGITAL_RESTART{0x3201};
	static constexpr uint16_t REGISTER_DIGITAL_DUAL_BINNING{0x3202};
	static constexpr uint16_t REGISTER_DIGITAL_SUBSAMPLE_RATIO{0x3204};
	static constexpr uint16_t REGISTER_DIGITAL_AREA_BLOCK{0x3205};
	static constexpr uint16_t REGISTER_DIGITAL_TIMESTAMP_SUBUNIT{0x3234};
	static constexpr uint16_t REGISTER_DIGITAL_TIMESTAMP_REFUNIT{0x3235};
	static constexpr uint16_t REGISTER_DIGITAL_DTAG_REFERENCE{0x323D};
	static constexpr uint16_t REGISTER_DIGITAL_TIMESTAMP_RESET{0x3238};
	static constexpr uint16_t REGISTER_TIMING_FIRST_SELX_START{0x323C};
	static constexpr uint16_t REGISTER_TIMING_GH_COUNT{0x3240};
	static constexpr uint16_t REGISTER_TIMING_GH_COUNT_FINE{0x3243};
	static constexpr uint16_t REGISTER_TIMING_GRS_COUNT{0x3244};
	static constexpr uint16_t REGISTER_TIMING_GRS_COUNT_FINE{0x3247};
	static constexpr uint16_t REGISTER_DIGITAL_GLOBAL_RESET_READOUT{0x3248};
	static constexpr uint16_t REGISTER_TIMING_NEXT_GH_CNT{0x324B};
	static constexpr uint16_t REGISTER_TIMING_SELX_WIDTH{0x324C};
	static constexpr uint16_t REGISTER_TIMING_AY_START{0x324E};
	static constexpr uint16_t REGISTER_TIMING_AY_END{0x324F};
	static constexpr uint16_t REGISTER_TIMING_MAX_EVENT_NUM{0x3251};
	static constexpr uint16_t REGISTER_TIMING_R_START{0x3253};
	static constexpr uint16_t REGISTER_TIMING_R_END{0x3254};
	static constexpr uint16_t REGISTER_DIGITAL_MODE_CONTROL{0x3255};
	static constexpr uint16_t REGISTER_TIMING_GRS_END{0x3256};
	static constexpr uint16_t REGISTER_TIMING_GRS_END_FINE{0x3259};
	static constexpr uint16_t REGISTER_DIGITAL_FIXED_READ_TIME{0x325C};
	static constexpr uint16_t REGISTER_TIMING_READ_TIME_INTERVAL{0x325D};
	static constexpr uint16_t REGISTER_DIGITAL_EXTERNAL_TRIGGER{0x3260};
	static constexpr uint16_t REGISTER_TIMING_NEXT_SELX_START{0x3261};
	static constexpr uint16_t REGISTER_DIGITAL_BOOT_SEQUENCE{0x3266};

	static constexpr uint16_t REGISTER_CROPPER_BYPASS{0x3300};
	static constexpr uint16_t REGISTER_CROPPER_Y_START_GROUP{0x3301};
	static constexpr uint16_t REGISTER_CROPPER_Y_START_MASK{0x3302};
	static constexpr uint16_t REGISTER_CROPPER_Y_END_GROUP{0x3303};
	static constexpr uint16_t REGISTER_CROPPER_Y_END_MASK{0x3304};
	static constexpr uint16_t REGISTER_CROPPER_X_START_ADDRESS{0x3305};
	static constexpr uint16_t REGISTER_CROPPER_X_END_ADDRESS{0x3307};

	static constexpr uint16_t REGISTER_ACTIVITY_DECISION_BYPASS{0x3500};

	static constexpr uint16_t REGISTER_SPATIAL_HISTOGRAM_OFF{0x3600};

	static constexpr uint16_t DVS_CHIP_MODE_OFF{0};
	static constexpr uint16_t DVS_CHIP_MODE_MONITOR{1};
	static constexpr uint16_t DVS_CHIP_MODE_STREAM{2};

	static constexpr uint16_t DVS_CHIP_DTAG_CONTROL_STOP{0};
	static constexpr uint16_t DVS_CHIP_DTAG_CONTROL_START{1};
	static constexpr uint16_t DVS_CHIP_DTAG_CONTROL_RESTART{2};

	static constexpr uint16_t SYSTEM_CLOCK_FREQUENCY{50};

	static constexpr uint16_t PID_DVXPLORER{0x8419};
	static constexpr uint8_t FIRMWARE_REQUIRED_VERSION{9};
	static constexpr uint32_t LOGIC_REQUIRED_VERSION{18};
	static constexpr uint32_t LOGIC_MINIMUM_PATCH{4};

	static constexpr uint32_t USB_EARLY_PACKET_DELAY_MAX{(1 << 20) - 1};
	static constexpr uint32_t EXT_INPUT_TIME_MAX{(1 << 20) - 1};

	static constexpr auto COMPATIBLE_CAMERA
		= [](const uint16_t vid, const uint16_t pid, const USBDeviceType deviceType) -> std::optional<CameraModel> {
		if ((vid == VID_INIVATION) && (pid == PID_DVXPLORER) && (deviceType == USBDeviceType::FX3_RED)) {
			return CameraModel::DVXPLORER;
		}

		return std::nullopt;
	};

	// Set during construction.
	uint32_t mLogicVersion;
	uint32_t mLogicPatch;
	cv::Size mResolution;
	SensorModel mSensorModel;
	imu::ImuModel mImuModel;
	float mLogicClockActual;
	float mUSBClockActual;
	std::unique_ptr<parser::DVXplorer::Parser> mParser;
	// Multi-part configuration needing locking.
	mutable std::mutex mConfigLock;
	ReadoutFPS mReadoutFPS;
	uint8_t mContrastThresholdOn;
	uint8_t mContrastThresholdOff;
	cv::Rect mCropArea;
	// Atomic due to USB callback use.
	std::atomic<bool> mIsRunning{true};
	std::atomic<bool> mTimestampMaster{true};

	void shutdownCallback() {
		mIsRunning = false;
	}

	void usbDataCallback(const std::span<const uint8_t> data) {
		// Return right away if not running anymore. This prevents useless work.
		if (!mIsRunning || !isUSBDataTransferRunning()) {
			return;
		}

		try {
			mParser->parseData(data, false);
		}
		catch (const dv::exceptions::RuntimeError &ex) {
			// Critical parsing failure (eg. timestamp error). Disable camera by setting mIsRunning=false.
			// Since we are inside the USB callback, we cannot stop the USB transfers from here. They'll continue
			// running but not be actually parsed since we are in an unrecoverable state only fit for destruction.
			mIsRunning = false;
			log(LogLevel::LVL_ERROR, ex.what());
		}
	}

	void dataParserCallback(parser::ParsedData data) {
		mBuffers.acceptData(std::move(data));
	}

	void timeInitCallback() {
		mSyncState = SyncState::GOT_RESET;

		mBuffers.clearBuffers();

		// Update Master/Slave status on incoming TS resets.
		// Async call to not deadlock here.
		spiConfigReceiveAsync(MODULE_SYSINFO, SYSINFO_DEVICE_IS_MASTER,
			[this](const libusb_transfer_status status, const uint32_t param) {
				// If any USB error happened, discard.
				if (status != LIBUSB_TRANSFER_COMPLETED) {
					return;
				}

				mTimestampMaster = param;
			});
	}
};
} // namespace dv::io::camera

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

template<>
struct fmt::formatter<dv::io::camera::DVXplorer::ReadoutFPS> : fmt::ostream_formatter {};
