#pragma once

#include "camera_input_base.hpp"
#include "dvxplorer.hpp"
#include "imu_support.hpp"
#include "parsers/s5k231y_parser.hpp"
#include "parsers/s5krc1s_parser.hpp"
#include "usb_device_nextgen.hpp"

namespace dv::io::camera {
class DVXplorerM final : public USBDeviceNextGen, public CameraInputBase {
public:
	/**
	 * Find connected DVXplorer Mini/Micro 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 Mini/Micro 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 DVXplorerM(loggerCallbackType deviceLogger = {}, const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVXplorerM(findDevices().at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVXplorer Mini/Micro 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 DVXplorerM(const std::string_view filterBySerialNumber, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVXplorerM(findDevices(filterBySerialNumber).at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVXplorer Mini/Micro 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 DVXplorerM(const DeviceDescriptor &deviceToOpen, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		USBDeviceNextGen(deviceToOpen, std::move(deviceLogger), deviceLogLevel, "dvx_m", "DVXplorerM",
			COMPATIBLE_CAMERA, FIRMWARE_REQUIRED_VERSION) {
		// Get sensor resolution from device.
		mResolution.width  = static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_RESOLUTION_X));
		mResolution.height = static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_RESOLUTION_Y));

		mSensorModel = (mResolution.width == parser::S5KRC1S::WIDTH) ? (SensorModel::S5KRC1S) : (SensorModel::S5K231Y);

		// Get IMU model information from device.
		mImuModel = dv::IntegerAsEnum<imu::ImuModel>(spiConfigReceive(MODULE_IMU, IMU_MODEL));

		// USB handling callbacks.
		setDataCallback(std::bind_front(&DVXplorerM::usbDataCallback, this));
		setShutdownCallback(std::bind_front(&DVXplorerM::shutdownCallback, this));
		setDebugCallback(std::bind_front(&DVXplorerM::usbDebugCallback, this));

		// Setup data parser.
		if (mSensorModel == SensorModel::S5KRC1S) {
			mParser = std::make_unique<parser::S5KRC1S::Parser>();
		}
		else {
			mParser = std::make_unique<parser::S5K231Y::Parser>();
		}

		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(&DVXplorerM::dataParserCallback, this));
		mParser->setTimeInitCallback(std::bind_front(&DVXplorerM::timeInitCallback, this));

		// Device default configuration.
		// DVS sensor defaults.
		if (mSensorModel == SensorModel::S5K231Y) {
			setReadoutFPS(DVXplorer::ReadoutFPS::VARIABLE_5000);
			setContrastThresholdOn(9);
			setContrastThresholdOff(9);
		}
		else {
			setContrastThresholdOn(70);
			setContrastThresholdOff(50);
		}

		setFlatten(Flatten::NONE);
		setSubSampleHorizontal(DVXplorer::SubSample::EVERY_PIXEL);
		setSubSampleVertical(DVXplorer::SubSample::EVERY_PIXEL);
		setGlobalHold(true);
		setGlobalReset(false);
		setGlobalResetSkip(0);
		setCropArea({
			{0, 0},
            mResolution
        });
		setMIPITimeoutEnable(true);
		setMIPITimeoutValue(std::chrono::microseconds{2000});

		// DVS sensor default orientation.
		setFlipHorizontal(getDVSOrientationX() == 'X');
		setFlipVertical(getDVSOrientationY() == 'Y');

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

		usbDebugTransfersStart();

		usbDataTransfersStart();

		try {
			// Start all data producers.
			spiConfigSend(MODULE_DVS, DVS_RUN, true);

			setImuRunningAccelerometer(true);
			setImuRunningGyroscope(true);
			setImuRunningTemperature(true);
		}
		catch (const dv::exceptions::RuntimeError &) {
			usbDebugTransfersStop();

			usbDataTransfersStop();

			throw;
		}
	}

	~DVXplorerM() override {
		try {
			// Stop all data producers.
			spiConfigSend(MODULE_DVS, DVS_RUN, false);

			setImuRunningAccelerometer(false);
			setImuRunningGyroscope(false);
			setImuRunningTemperature(false);
		}
		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();
	}

	/**
	 * Load configuration from on-device flash memory.
	 */
	void configLoadFromFlash() {
		usbControlTransferOut(VR_CONFIG_RELOAD, 0, 0, {});

		// Get information from device and apply to parser.
		if (mSensorModel == SensorModel::S5K231Y) {
			dynamic_cast<parser::S5K231Y::Parser *>(mParser.get())->setFlipHorizontal(getFlipHorizontal());
			dynamic_cast<parser::S5K231Y::Parser *>(mParser.get())->setFlipVertical(getFlipVertical());
		}
	}

	/**
	 * Store current configuration to on-device flash memory.
	 */
	void configStoreToFlash() {
		usbControlTransferOut(VR_CONFIG_STORE, 0, 0, {});
	}

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

	[[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::S5KRC1S) {
			// S5KRC1S.
			return 4.95e-6f;
		}
		else {
			// S5K231Y.
			return 9e-6f;
		}
	}

	[[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 false;
	}

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

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

		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");
	}

	[[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();
	}

	[[nodiscard]] bool getFlipHorizontal() const override {
		return spiConfigReceive(MODULE_DVS, DVS_FLIP_HORIZONTAL);
	}

	void setFlipHorizontal(const bool flipHorizontal) override {
		spiConfigSend(MODULE_DVS, DVS_FLIP_HORIZONTAL, flipHorizontal);

		if (mSensorModel == SensorModel::S5K231Y) {
			dynamic_cast<parser::S5K231Y::Parser *>(mParser.get())->setFlipHorizontal(flipHorizontal);
		}

		// Send new crop values to chip after relevant changes.
		spiConfigSend(MODULE_DVS, DVS_CROP_APPLY, 0);
	}

	[[nodiscard]] bool getFlipVertical() const override {
		return spiConfigReceive(MODULE_DVS, DVS_FLIP_VERTICAL);
	}

	void setFlipVertical(const bool flipVertical) override {
		spiConfigSend(MODULE_DVS, DVS_FLIP_VERTICAL, flipVertical);

		if (mSensorModel == SensorModel::S5K231Y) {
			dynamic_cast<parser::S5K231Y::Parser *>(mParser.get())->setFlipVertical(flipVertical);
		}

		// Send new crop values to chip after relevant changes.
		spiConfigSend(MODULE_DVS, DVS_CROP_APPLY, 0);
	}

	[[nodiscard]] Flatten getFlatten() const override {
		return dv::IntegerAsEnum<Flatten>(spiConfigReceive(MODULE_DVS, DVS_FLATTEN));
	}

	void setFlatten(const Flatten flatten) override {
		spiConfigSend(MODULE_DVS, DVS_FLATTEN, dv::EnumAsInteger(flatten));
	}

	/**
	 * Get horizontal subsampling mode.
	 * VGA sensor: all values.
	 * HD sensor: only EVERY_PIXEL, EVERY_SECOND, EVERY_FOURTH, EVERY_EIGHTH.
	 * @return current horizontal subsampling mode.
	 */
	[[nodiscard]] DVXplorer::SubSample getSubSampleHorizontal() const {
		return dv::IntegerAsEnum<DVXplorer::SubSample>(spiConfigReceive(MODULE_DVS, DVS_SUBSAMPLE_HORIZONTAL));
	}

	/**
	 * Set horizontal subsampling mode.
	 * VGA sensor: all values.
	 * HD sensor: only EVERY_PIXEL, EVERY_SECOND, EVERY_FOURTH, EVERY_EIGHTH.
	 * @param subSampleHorizontal new horizontal subsampling mode.
	 */
	void setSubSampleHorizontal(const DVXplorer::SubSample subSampleHorizontal) {
		spiConfigSend(MODULE_DVS, DVS_SUBSAMPLE_HORIZONTAL, dv::EnumAsInteger(subSampleHorizontal));
	}

	/**
	 * Get vertical subsampling mode.
	 * VGA sensor: all values.
	 * HD sensor: only EVERY_PIXEL, EVERY_SECOND, EVERY_FOURTH, EVERY_EIGHTH.
	 * @return current vertical subsampling mode.
	 */
	[[nodiscard]] DVXplorer::SubSample getSubSampleVertical() const {
		return dv::IntegerAsEnum<DVXplorer::SubSample>(spiConfigReceive(MODULE_DVS, DVS_SUBSAMPLE_VERTICAL));
	}

	/**
	 * Set vertical subsampling mode.
	 * VGA sensor: all values.
	 * HD sensor: only EVERY_PIXEL, EVERY_SECOND, EVERY_FOURTH, EVERY_EIGHTH.
	 * @param subSampleVertical new vertical subsampling mode.
	 */
	void setSubSampleVertical(const DVXplorer::SubSample subSampleVertical) {
		spiConfigSend(MODULE_DVS, DVS_SUBSAMPLE_VERTICAL, dv::EnumAsInteger(subSampleVertical));
	}

	/**
	 * Report status of global hold feature.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGlobalHold() const {
		return spiConfigReceive(MODULE_DVS, DVS_GLOBAL_HOLD);
	}

	/**
	 * 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) {
		spiConfigSend(MODULE_DVS, DVS_GLOBAL_HOLD, globalHold);
	}

	/**
	 * Report status of global reset feature.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGlobalReset() const {
		return spiConfigReceive(MODULE_DVS, DVS_GLOBAL_RESET);
	}

	/**
	 * Enable or disable global reset feature.
	 * @param globalReset true to enable, false to disable.
	 */
	void setGlobalReset(const bool globalReset) {
		spiConfigSend(MODULE_DVS, DVS_GLOBAL_RESET, globalReset);
	}

	/**
	 * Get global reset skip value.
	 * @return only perform global reset every N+1 readout frames.
	 */
	[[nodiscard]] uint8_t getGlobalResetSkip() const {
		return spiConfigReceive(MODULE_DVS, DVS_GLOBAL_RESET_SKIP);
	}

	/**
	 * Control global reset skip feature.
	 * Only perform global reset, if enabled, every N+1 readout frames.
	 * Setting to zero disables this feature.
	 * @param globalResetSkip only perform global reset every N+1 readout frames.
	 */
	void setGlobalResetSkip(const uint8_t globalResetSkip) {
		spiConfigSend(MODULE_DVS, DVS_GLOBAL_RESET_SKIP, globalResetSkip);
	}

	/**
	 * Report status of MIPI timeout feature.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getMIPITimeoutEnable() const {
		return spiConfigReceive(MODULE_DVS, DVS_MIPI_TIMEOUT_ENABLE);
	}

	/**
	 * Enable MIPI timeout, will ensure data is sent from sensor to host at least every N microseconds, even during
	 * periods of low activity. See setMIPITimeoutValue() to set the timeout value.
	 * @param mipiTimeoutEnable true to enable, false to disable.
	 */
	void setMIPITimeoutEnable(const bool mipiTimeoutEnable) {
		spiConfigSend(MODULE_DVS, DVS_MIPI_TIMEOUT_ENABLE, mipiTimeoutEnable);
	}

	/**
	 * Get MIPI timeout in microseconds.
	 * @return timeout in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getMIPITimeoutValue() const {
		return std::chrono::microseconds{spiConfigReceive(MODULE_DVS, DVS_MIPI_TIMEOUT_VALUE)};
	}

	/**
	 * Set MIPI timeout in microseconds.
	 * See setMIPITimeoutEnable() to enable this functionality.
	 * @param mipiTimeoutValueUs timeout in microseconds.
	 */
	void setMIPITimeoutValue(const std::chrono::microseconds mipiTimeoutValueUs) {
		spiConfigSend(MODULE_DVS, DVS_MIPI_TIMEOUT_VALUE, mipiTimeoutValueUs.count());
	}

	/**
	 * Get currently set event-frame readout frequency.
	 * Not available on HD sensor.
	 * @return event-frame readout frequency.
	 */
	[[nodiscard]] DVXplorer::ReadoutFPS getReadoutFPS() const {
		if (mSensorModel == SensorModel::S5KRC1S) {
			throw dv::exceptions::RuntimeError("Readout FPS feature not available.");
		}

		return dv::IntegerAsEnum<DVXplorer::ReadoutFPS>(spiConfigReceive(MODULE_DVS, DVS_EFPS_S5K231Y));
	}

	/**
	 * Set frequency of event-frame readouts on sensor.
	 * Not available on HD 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 DVXplorer::ReadoutFPS readoutFps) {
		if (mSensorModel == SensorModel::S5KRC1S) {
			throw dv::exceptions::RuntimeError("Readout FPS feature not available.");
		}

		spiConfigSend(MODULE_DVS, DVS_EFPS_S5K231Y, dv::EnumAsInteger(readoutFps));
	}

	/**
	 * Get the contrast threshold for ON polarity event generation.
	 * VGA sensor: values from 0 to 17.
	 * HD sensor: values from 0 to 127.
	 * @return ON contrast threshold.
	 */
	uint8_t getContrastThresholdOn() const {
		return spiConfigReceive(MODULE_DVS, DVS_CONTRAST_THRESHOLD_ON);
	}

	/**
	 * Set the contrast threshold for ON polarity event generation.
	 * VGA sensor: values from 0 to 17.
	 * HD sensor: values from 0 to 127.
	 * @param contrastThresholdOn ON contrast threshold.
	 */
	void setContrastThresholdOn(const uint8_t contrastThresholdOn) {
		spiConfigSend(MODULE_DVS, DVS_CONTRAST_THRESHOLD_ON, contrastThresholdOn);
	}

	/**
	 * Get the contrast threshold for OFF polarity event generation.
	 * VGA sensor: values from 0 to 17.
	 * HD sensor: values from 0 to 127.
	 * @return OFF contrast threshold.
	 */
	[[nodiscard]] uint8_t getContrastThresholdOff() const {
		return spiConfigReceive(MODULE_DVS, DVS_CONTRAST_THRESHOLD_OFF);
	}

	/**
	 * Set the contrast threshold for OFF polarity event generation.
	 * VGA sensor: values from 0 to 17.
	 * HD sensor: values from 0 to 127.
	 * @param contrastThresholdOff OFF contrast threshold.
	 */
	void setContrastThresholdOff(const uint8_t contrastThresholdOff) {
		spiConfigSend(MODULE_DVS, DVS_CONTRAST_THRESHOLD_OFF, contrastThresholdOff);
	}

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

		const auto x = spiConfigReceive(MODULE_DVS, DVS_CROP_X);
		const auto y = spiConfigReceive(MODULE_DVS, DVS_CROP_Y);

		const auto w = spiConfigReceive(MODULE_DVS, DVS_CROP_WIDTH);
		const auto h = spiConfigReceive(MODULE_DVS, DVS_CROP_HEIGHT);

		return cv::Rect{static_cast<int>(x), static_cast<int>(y), static_cast<int>(w), static_cast<int>(h)};
	}

	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};

		spiConfigSend(MODULE_DVS, DVS_CROP_X, cropArea.x);
		spiConfigSend(MODULE_DVS, DVS_CROP_Y, cropArea.y);

		spiConfigSend(MODULE_DVS, DVS_CROP_WIDTH, cropArea.width);
		spiConfigSend(MODULE_DVS, DVS_CROP_HEIGHT, cropArea.height);

		spiConfigSend(MODULE_DVS, DVS_CROP_APPLY, 0);
	}

	/**
	 * Report if the pixel block at the given X,Y address is filtered out or not.
	 * @param blockX horizontal address of pixel block.
	 * VGA sensor: from 0 to 19, pixel block size 32x32.
	 * HD sensor: from 0 to 23, pixel block size 40x40.
	 * @param blockY vertical address of pixel block.
	 * VGA sensor: from 0 to 14, pixel block size 32x32.
	 * HD sensor: from 0 to 17, pixel block size 40x40.
	 * @return true if filtered out, false if normal operation.
	 */
	[[nodiscard]] bool getAreaBlock(const uint32_t blockX, const uint32_t blockY) const {
		std::scoped_lock lock{mConfigLock};

		spiConfigSend(MODULE_DVS, DVS_AREA_BLOCKING_X_BLOCK, blockX);
		spiConfigSend(MODULE_DVS, DVS_AREA_BLOCKING_Y_BLOCK, blockY);

		return spiConfigReceive(MODULE_DVS, DVS_AREA_BLOCKING_BLOCK);
	}

	/**
	 * Enable pixel block filtering at the given X,Y address.
	 * @param blockX horizontal address of pixel block.
	 * VGA sensor: from 0 to 19, pixel block size 32x32.
	 * HD sensor: from 0 to 23, pixel block size 40x40.
	 * @param blockY vertical address of pixel block.
	 * VGA sensor: from 0 to 14, pixel block size 32x32.
	 * HD sensor: from 0 to 17, pixel block size 40x40.
	 * @param block true to filter out, false for normal operation.
	 */
	void setAreaBlock(const uint32_t blockX, const uint32_t blockY, const bool block) {
		std::scoped_lock lock{mConfigLock};

		spiConfigSend(MODULE_DVS, DVS_AREA_BLOCKING_X_BLOCK, blockX);
		spiConfigSend(MODULE_DVS, DVS_AREA_BLOCKING_Y_BLOCK, blockY);

		spiConfigSend(MODULE_DVS, DVS_AREA_BLOCKING_BLOCK, block);
	}

	/**
	 * 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);
	}

	/**
	 * 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));
	}

	/**
	 * 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));
	}

	/**
	 * 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 {
		return dv::IntegerAsEnum<imu::BoschBMI160AccelRange>(spiConfigReceive(MODULE_IMU, IMU_ACCEL_RANGE));
	}

	/**
	 * Set IMU accelerometer range.
	 * @param range accelerometer range.
	 */
	void setImuAccelRange(const imu::BoschBMI160AccelRange range) {
		spiConfigSend(MODULE_IMU, IMU_ACCEL_RANGE, dv::EnumAsInteger(range));
	}

	/**
	 * 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));
	}

	/**
	 * 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));
	}

	/**
	 * 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));
	}

private:
	enum class SensorModel {
		S5K231Y = 0,
		S5KRC1S = 1,
	};

	static constexpr uint8_t MODULE_DVS{1};
	static constexpr uint8_t DVS_RESOLUTION_X{0};
	static constexpr uint8_t DVS_RESOLUTION_Y{1};
	static constexpr uint8_t DVS_ORIENTATION{2};
	static constexpr uint8_t DVS_RUN{3};
	static constexpr uint8_t DVS_FLATTEN{4};
	static constexpr uint8_t DVS_SUBSAMPLE_HORIZONTAL{5};
	static constexpr uint8_t DVS_SUBSAMPLE_VERTICAL{6};
	// static constexpr uint8_t DVS_DUAL_BINNING_S5K231Y{7};
	static constexpr uint8_t DVS_GLOBAL_HOLD{8};
	static constexpr uint8_t DVS_GLOBAL_RESET{9};
	static constexpr uint8_t DVS_GLOBAL_RESET_SKIP{10};
	static constexpr uint8_t DVS_MIPI_TIMEOUT_ENABLE{11};
	static constexpr uint8_t DVS_MIPI_TIMEOUT_VALUE{12};
	static constexpr uint8_t DVS_EFPS_S5K231Y{13};
	static constexpr uint8_t DVS_CONTRAST_THRESHOLD_ON{14};
	static constexpr uint8_t DVS_CONTRAST_THRESHOLD_OFF{15};
	static constexpr uint8_t DVS_AREA_BLOCKING_X_BLOCK{16};
	static constexpr uint8_t DVS_AREA_BLOCKING_Y_BLOCK{17};
	static constexpr uint8_t DVS_AREA_BLOCKING_BLOCK{18};
	static constexpr uint8_t DVS_CROP_X{19};
	static constexpr uint8_t DVS_CROP_Y{20};
	static constexpr uint8_t DVS_CROP_WIDTH{21};
	static constexpr uint8_t DVS_CROP_HEIGHT{22};
	static constexpr uint8_t DVS_CROP_APPLY{23};
	static constexpr uint8_t DVS_FLIP_HORIZONTAL{24};
	static constexpr uint8_t DVS_FLIP_VERTICAL{25};

	static constexpr uint8_t MODULE_IMU{3};
	static constexpr uint8_t IMU_MODEL{0};
	static constexpr uint8_t IMU_ORIENTATION{1};
	static constexpr uint8_t IMU_RUN_ACCELEROMETER{2};
	static constexpr uint8_t IMU_RUN_GYROSCOPE{3};
	static constexpr uint8_t IMU_RUN_TEMPERATURE{4};
	static constexpr uint8_t IMU_ACCEL_DATA_RATE{5};
	static constexpr uint8_t IMU_ACCEL_FILTER{6};
	static constexpr uint8_t IMU_ACCEL_RANGE{7};
	static constexpr uint8_t IMU_GYRO_DATA_RATE{8};
	static constexpr uint8_t IMU_GYRO_FILTER{9};
	static constexpr uint8_t IMU_GYRO_RANGE{10};
	static constexpr uint8_t IMU_FOC_RUN{11};
	static constexpr uint8_t IMU_FOC_ACCEL_X{12};
	static constexpr uint8_t IMU_FOC_ACCEL_Y{13};
	static constexpr uint8_t IMU_FOC_ACCEL_Z{14};
	static constexpr uint8_t IMU_FOC_GYRO_X{15};
	static constexpr uint8_t IMU_FOC_GYRO_Y{16};
	static constexpr uint8_t IMU_FOC_GYRO_Z{17};
	static constexpr uint8_t IMU_FOC_APPLY{18};

	static constexpr uint8_t VR_CONFIG_STORE{0xC7};
	static constexpr uint8_t VR_CONFIG_RELOAD{0xC8};

	static constexpr uint16_t PID_DVXPLORER{0x8419};
	static constexpr uint8_t FIRMWARE_REQUIRED_VERSION{10};

	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::CX3_MIPI)) {
			return CameraModel::DVXPLORER_M;
		}

		return std::nullopt;
	};

	// Set during construction.
	cv::Size mResolution;
	SensorModel mSensorModel;
	imu::ImuModel mImuModel;
	std::unique_ptr<parser::ParserBase> mParser;
	// Multi-part configuration needing locking.
	mutable std::mutex mConfigLock;
	// Atomic due to USB callback use.
	std::atomic<bool> mIsRunning{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;
		}

		// TODO: track data loss on USB CX3 state-machine.
		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 usbDebugCallback(const std::span<const uint8_t> data) const {
		if (!mIsRunning) {
			return;
		}

		if (data[0] != 0x01) {
			// Only process IMU messages, code 0x01.
			return;
		}

		// Set correct IMU accel and gyro scales, used to interpret subsequent IMU samples from the device.
		const auto accelScale = imu::boschBMI160CalculateIMUAccelScale((data[1] >> 3) & 0x03);
		const auto gyroScale  = imu::boschBMI160CalculateIMUGyroScale(data[1] & 0x07);

		dv::IMU imuData{};

		if ((data[1] >> 7) & 0x01) {
			const auto accelX      = boost::endian::load_little_s16(&data[8]);
			imuData.accelerometerX = static_cast<float>(accelX) / accelScale;

			const auto accelY      = boost::endian::load_little_s16(&data[10]);
			imuData.accelerometerY = static_cast<float>(accelY) / accelScale;

			const auto accelZ      = boost::endian::load_little_s16(&data[12]);
			imuData.accelerometerZ = static_cast<float>(accelZ) / accelScale;
		}

		if ((data[1] >> 6) & 0x01) {
			const auto gyroX   = boost::endian::load_little_s16(&data[2]);
			imuData.gyroscopeX = static_cast<float>(gyroX) / gyroScale;

			const auto gyroY   = boost::endian::load_little_s16(&data[4]);
			imuData.gyroscopeY = static_cast<float>(gyroY) / gyroScale;

			const auto gyroZ   = boost::endian::load_little_s16(&data[6]);
			imuData.gyroscopeZ = static_cast<float>(gyroZ) / gyroScale;
		}

		if ((data[1] >> 5) & 0x01) {
			// Temperature is signed. Formula for converting to °C:
			// (SIGNED_VAL / 512) + 23
			const auto temp     = boost::endian::load_little_s16(&data[14]);
			imuData.temperature = (static_cast<float>(temp) / 512.0f) + 23.0f;
		}

		// Inject IMU sample into parser for timestamping.
		if (mSensorModel == SensorModel::S5KRC1S) {
			dynamic_cast<parser::S5KRC1S::Parser *>(mParser.get())->injectImu(imuData);
		}
		else {
			dynamic_cast<parser::S5K231Y::Parser *>(mParser.get())->injectImu(imuData);
		}
	}

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

	void timeInitCallback() {
		mBuffers.clearBuffers();
	}

	[[nodiscard]] char getDVSOrientationX() const {
		return static_cast<char>((spiConfigReceive(MODULE_DVS, DVS_ORIENTATION) >> 8) & 0xFF);
	}

	[[nodiscard]] char getDVSOrientationY() const {
		return static_cast<char>(spiConfigReceive(MODULE_DVS, DVS_ORIENTATION) & 0xFF);
	}
};
} // namespace dv::io::camera
