#include "mvcamera.hpp"

#include <glib.h>
#include <regex>

static const std::regex nameCleanupRegex{"[^a-zA-Z-_\\d]"};

void *dvModuleGetHooks(enum dvModuleHooks hook) {
	// Support only device discovery.
	if (hook != DV_HOOK_DEVICE_DISCOVERY) {
		return nullptr;
	}

	// Get current devices from Aravis library.
	arv_update_device_list();
	auto n_devices = arv_get_n_devices();

	// Add devices to config tree.
	auto devicesNode = dv::Cfg::GLOBAL.getNode("/system/_devices/");

	for (unsigned int i = 0; i < n_devices; i++) {
		const auto deviceName = std::regex_replace(arv_get_device_id(i), nameCleanupRegex, "_");

		const auto nodeName = fmt::format(FMT_STRING("mv_{:s}/"), deviceName);

		auto devNode = devicesNode.getRelativeNode(nodeName);

		devNode.create<dv::CfgType::STRING>("OpenWithModule", "dv_mvcamera", {1, 32},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Open device with specified module.");

		devNode.create<dv::CfgType::STRING>("DeviceID", arv_get_device_id(i), {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device ID.");

		devNode.create<dv::CfgType::STRING>("Protocol",
			(arv_get_device_protocol(i) == nullptr) ? ("") : (arv_get_device_protocol(i)), {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device protocol.");
		devNode.create<dv::CfgType::STRING>("SerialNumber",
			(arv_get_device_serial_nbr(i) == nullptr) ? ("") : (arv_get_device_serial_nbr(i)), {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device serial number.");

		devNode.create<dv::CfgType::STRING>("Vendor",
			(arv_get_device_vendor(i) == nullptr) ? ("") : (arv_get_device_vendor(i)), {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device vendor.");
		devNode.create<dv::CfgType::STRING>("Model",
			(arv_get_device_model(i) == nullptr) ? ("") : (arv_get_device_model(i)), {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device model.");
	}

	// Nothing to return, no further steps needed.
	return nullptr;
}

const char *MvCamera::initDescription() {
	return "Machine Vision Camera (GenICam standard) module.";
}

void MvCamera::initOutputs(dv::OutputDefinitionList &out) {
	out.addFrameOutput("frames");
}

void MvCamera::initConfigOptions(dv::RuntimeConfig &config) {
	config.add("deviceID",
		dv::ConfigOption::stringOption("Device ID of camera to open (if empty, open first available camera).", ""));

	config.add(
		"frameRate", dv::ConfigOption::doubleOption("Number of frames per second to acquire.", 30.0, 1.0, 1000.0));

	config.add("binningX", dv::ConfigOption::intOption("Binning value for the X axis.", 1, 1, 64));
	config.add("binningY", dv::ConfigOption::intOption("Binning value for the Y axis.", 1, 1, 64));

	config.add(
		"roiOffsetX", dv::ConfigOption::intOption("Region-of-Interst offset value for the X axis.", 0, 0, (8192 - 1)));
	config.add(
		"roiOffsetY", dv::ConfigOption::intOption("Region-of-Interst offset value for the Y axis.", 0, 0, (6144 - 1)));
	config.add("roiWidth", dv::ConfigOption::intOption("Region-of-Interst width (X axis).", 8192, 1, 8192));
	config.add("roiHeight", dv::ConfigOption::intOption("Region-of-Interst height (Y axis).", 6144, 1, 6144));

	config.add("autoExposure", dv::ConfigOption::boolOption("Enable automatic exposure time control.", true));
	config.add(
		"exposure", dv::ConfigOption::doubleOption("Manual exposure time control (in µs).", 5000.0, 1.0, 1000000.0));

	config.add("autoGain", dv::ConfigOption::boolOption("Enable automatic gain control.", true));
	config.add("gain", dv::ConfigOption::doubleOption("Manual gain control.", 0.0, 0.0, 100.0));

	config.add("colorMode", dv::ConfigOption::boolOption("Select between grayscale and color frames.", true));

	config.setPriorityOptions({"frameRate", "colorMode"});
}

MvCamera::MvCamera() {
	// Aravis error handling (glib).
	GError *error = nullptr;

	// Open camera based on given device ID.
	const auto deviceId = config.getString("deviceID");

	if (deviceId.empty()) {
		camera = arv_camera_new(nullptr, &error);
	}
	else {
		camera = arv_camera_new(deviceId.c_str(), &error);
	}

	if (error != nullptr) {
		int code            = error->code;
		std::string message = error->message;
		g_clear_error(&error);

		throw std::invalid_argument(fmt::format("arv_camera_new failed, error {} (code {}).", message, code));
	}

	if ((camera == nullptr) || (!static_cast<bool>(ARV_IS_CAMERA(static_cast<gpointer>(camera))))) {
		throw std::invalid_argument("Could not initialize camera.");
	}

	auto deviceName = deviceId;

	if (deviceName.empty()) {
		const auto deviceNameFromCamera = fmt::format("{}-{}-{}", arv_camera_get_vendor_name(camera, nullptr),
			arv_camera_get_model_name(camera, nullptr), arv_camera_get_device_id(camera, nullptr));
		deviceName                      = std::regex_replace(deviceNameFromCamera, nameCleanupRegex, "_");
	}

	log.info.format("Opened device {}.", deviceName);

	// Extract sensor size and setup frame output.
	gint sensorWidth;
	gint sensorHeight;

	arv_camera_get_sensor_size(camera, &sensorWidth, &sensorHeight, &error);
	if (error != nullptr) {
		int code            = error->code;
		std::string message = error->message;
		g_clear_error(&error);

		throw std::runtime_error(fmt::format("arv_camera_get_sensor_size failed, error {} (code {}).", message, code));
	}

	outputs.getFrameOutput("frames").setup(sensorWidth, sensorHeight, deviceName);

	// ROI support (seems to be always available).
	gint roiBoundsOffsetXMin;
	gint roiBoundsOffsetXMax;

	arv_camera_get_x_offset_bounds(camera, &roiBoundsOffsetXMin, &roiBoundsOffsetXMax, &error);
	if (error != nullptr) {
		roiBoundsOffsetXMin = 0;
		roiBoundsOffsetXMax = (sensorWidth - 1);

		log.error.format("arv_camera_get_x_offset_bounds failed, error {} (code {}).", error->message, error->code);
		g_clear_error(&error);
	}

	// Sanity check.
	if ((roiBoundsOffsetXMin < 0) || (roiBoundsOffsetXMax > (sensorWidth - 1))
		|| (roiBoundsOffsetXMin >= roiBoundsOffsetXMax)) {
		roiBoundsOffsetXMin = 0;
		roiBoundsOffsetXMax = (sensorWidth - 1);
	}

	gint roiBoundsOffsetYMin;
	gint roiBoundsOffsetYMax;

	arv_camera_get_y_offset_bounds(camera, &roiBoundsOffsetYMin, &roiBoundsOffsetYMax, &error);
	if (error != nullptr) {
		roiBoundsOffsetYMin = 0;
		roiBoundsOffsetYMax = (sensorHeight - 1);

		log.error.format("arv_camera_get_y_offset_bounds failed, error {} (code {}).", error->message, error->code);
		g_clear_error(&error);
	}

	// Sanity check.
	if ((roiBoundsOffsetYMin < 0) || (roiBoundsOffsetYMax > (sensorHeight - 1))
		|| (roiBoundsOffsetYMin >= roiBoundsOffsetYMax)) {
		roiBoundsOffsetYMin = 0;
		roiBoundsOffsetYMax = (sensorHeight - 1);
	}

	gint roiBoundsWidthMin;
	gint roiBoundsWidthMax;

	arv_camera_get_width_bounds(camera, &roiBoundsWidthMin, &roiBoundsWidthMax, &error);
	if (error != nullptr) {
		roiBoundsWidthMin = 1;
		roiBoundsWidthMax = sensorWidth;

		log.error.format("arv_camera_get_width_bounds failed, error {} (code {}).", error->message, error->code);
		g_clear_error(&error);
	}

	// Sanity check.
	if ((roiBoundsWidthMin < 1) || (roiBoundsWidthMax > sensorWidth) || (roiBoundsWidthMin >= roiBoundsWidthMax)) {
		roiBoundsWidthMin = 1;
		roiBoundsWidthMax = sensorWidth;
	}

	gint roiBoundsHeightMin;
	gint roiBoundsHeightMax;

	arv_camera_get_height_bounds(camera, &roiBoundsHeightMin, &roiBoundsHeightMax, &error);
	if (error != nullptr) {
		roiBoundsHeightMin = 1;
		roiBoundsHeightMax = sensorHeight;

		log.error.format("arv_camera_get_height_bounds failed, error {} (code {}).", error->message, error->code);
		g_clear_error(&error);
	}

	// Sanity check.
	if ((roiBoundsHeightMin < 1) || (roiBoundsHeightMax > sensorHeight) || (roiBoundsHeightMin >= roiBoundsHeightMax)) {
		roiBoundsHeightMin = 1;
		roiBoundsHeightMax = sensorHeight;
	}

	gint roiOffsetXCurrent;
	gint roiOffsetYCurrent;
	gint roiWidthCurrent;
	gint roiHeightCurrent;

	arv_camera_get_region(camera, &roiOffsetXCurrent, &roiOffsetYCurrent, &roiWidthCurrent, &roiHeightCurrent, &error);
	if (error != nullptr) {
		roiOffsetXCurrent = 0;
		roiOffsetYCurrent = 0;
		roiWidthCurrent   = sensorWidth;
		roiHeightCurrent  = sensorHeight;

		log.error.format("arv_camera_get_region failed, error {} (code {}).", error->message, error->code);
		g_clear_error(&error);
	}

	roiOffsetXCurrent = std::clamp(roiOffsetXCurrent, roiBoundsOffsetXMin, roiBoundsOffsetXMax);
	roiOffsetYCurrent = std::clamp(roiOffsetYCurrent, roiBoundsOffsetYMin, roiBoundsOffsetYMax);
	roiWidthCurrent   = std::clamp(roiWidthCurrent, roiBoundsWidthMin, roiBoundsWidthMax);
	roiHeightCurrent  = std::clamp(roiHeightCurrent, roiBoundsHeightMin, roiBoundsHeightMax);

	// Setup ROI ranges and controls.
	moduleNode.createAttribute<dv::CfgType::INT>("roiOffsetX", roiOffsetXCurrent,
		{roiBoundsOffsetXMin, roiBoundsOffsetXMax}, dv::CfgFlags::NORMAL,
		moduleNode.getAttributeDescription<dv::CfgType::INT>("roiOffsetX"));
	moduleNode.createAttribute<dv::CfgType::INT>("roiOffsetY", roiOffsetYCurrent,
		{roiBoundsOffsetYMin, roiBoundsOffsetYMax}, dv::CfgFlags::NORMAL,
		moduleNode.getAttributeDescription<dv::CfgType::INT>("roiOffsetY"));

	moduleNode.createAttribute<dv::CfgType::INT>("roiWidth", roiWidthCurrent, {roiBoundsWidthMin, roiBoundsWidthMax},
		dv::CfgFlags::NORMAL, moduleNode.getAttributeDescription<dv::CfgType::INT>("roiWidth"));
	moduleNode.createAttribute<dv::CfgType::INT>("roiHeight", roiHeightCurrent,
		{roiBoundsHeightMin, roiBoundsHeightMax}, dv::CfgFlags::NORMAL,
		moduleNode.getAttributeDescription<dv::CfgType::INT>("roiHeight"));

	// Extract basic feature availability.
	isFrameRateAvailable    = arv_camera_is_frame_rate_available(camera, nullptr);
	isBinningAvailable      = arv_camera_is_binning_available(camera, nullptr);
	isExposureAvailable     = arv_camera_is_exposure_time_available(camera, nullptr);
	isAutoExposureAvailable = arv_camera_is_exposure_auto_available(camera, nullptr);
	isGainAvailable         = arv_camera_is_gain_available(camera, nullptr);
	isAutoGainAvailable     = arv_camera_is_gain_auto_available(camera, nullptr);

	// Frame-rate control support.
	if (isFrameRateAvailable) {
		double frameRateMin;
		double frameRateMax;

		arv_camera_get_frame_rate_bounds(camera, &frameRateMin, &frameRateMax, &error);
		if (error != nullptr) {
			frameRateMin = 1.0;
			frameRateMax = 1000.0;

			log.error.format(
				"arv_camera_get_frame_rate_bounds failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		double frameRateCurrent = arv_camera_get_frame_rate(camera, &error);
		if (error != nullptr) {
			frameRateCurrent = 30.0;

			log.error.format("arv_camera_get_frame_rate failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		frameRateCurrent = std::clamp(frameRateCurrent, frameRateMin, frameRateMax);

		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("frameRate", frameRateCurrent, {frameRateMin, frameRateMax},
			dv::CfgFlags::NORMAL, moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("frameRate"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("frameRate", 30.0, {1.0, 1000.0}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("frameRate"));
	}

	// Binning support.
	if (isBinningAvailable) {
		gint binningBoundsXMin;
		gint binningBoundsXMax;

		arv_camera_get_x_binning_bounds(camera, &binningBoundsXMin, &binningBoundsXMax, &error);
		if (error != nullptr) {
			binningBoundsXMin = 1;
			binningBoundsXMax = 64;

			log.error.format(
				"arv_camera_get_x_binning_bounds failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		gint binningBoundsYMin;
		gint binningBoundsYMax;

		arv_camera_get_y_binning_bounds(camera, &binningBoundsYMin, &binningBoundsYMax, &error);
		if (error != nullptr) {
			binningBoundsYMin = 1;
			binningBoundsYMax = 64;

			log.error.format(
				"arv_camera_get_y_binning_bounds failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		gint binningCurrentX;
		gint binningCurrentY;

		arv_camera_get_binning(camera, &binningCurrentX, &binningCurrentY, &error);
		if (error != nullptr) {
			binningCurrentX = 1;
			binningCurrentY = 1;

			log.error.format("arv_camera_get_binning failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		binningCurrentX = std::clamp(binningCurrentX, binningBoundsXMin, binningBoundsXMax);
		binningCurrentY = std::clamp(binningCurrentY, binningBoundsYMin, binningBoundsYMax);

		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::INT>("binningX", binningCurrentX,
			{binningBoundsXMin, binningBoundsXMax}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::INT>("binningX"));
		moduleNode.createAttribute<dv::CfgType::INT>("binningY", binningCurrentY,
			{binningBoundsYMin, binningBoundsYMax}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::INT>("binningY"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::INT>("binningX", 1, {1, 64}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::INT>("binningX"));
		moduleNode.createAttribute<dv::CfgType::INT>("binningY", 1, {1, 64}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::INT>("binningY"));
	}

	// Exposure time control support.
	if (isAutoExposureAvailable) {
		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::BOOL>("autoExposure", true, {}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("autoExposure"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::BOOL>("autoExposure", false, {}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("autoExposure"));
		moduleNode.updateReadOnly<dv::CfgType::BOOL>("autoExposure", false);
	}

	if (isExposureAvailable) {
		double exposureBoundsTimeMin;
		double exposureBoundsTimeMax;

		arv_camera_get_exposure_time_bounds(camera, &exposureBoundsTimeMin, &exposureBoundsTimeMax, &error);
		if (error != nullptr) {
			exposureBoundsTimeMin = 1.0;
			exposureBoundsTimeMax = 1000000.0;

			log.error.format(
				"arv_camera_get_exposure_time_bounds failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		double exposureTimeCurrent = arv_camera_get_exposure_time(camera, &error);
		if (error != nullptr) {
			exposureTimeCurrent = 5000.0;

			log.error.format("arv_camera_get_exposure_time failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		exposureTimeCurrent = std::clamp(exposureTimeCurrent, exposureBoundsTimeMin, exposureBoundsTimeMax);

		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("exposure", exposureTimeCurrent,
			{exposureBoundsTimeMin, exposureBoundsTimeMax}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("exposure"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("exposure", 5000.0, {1.0, 1000000.0}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("exposure"));
	}

	// Gain control support.
	if (isAutoGainAvailable) {
		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::BOOL>("autoGain", true, {}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("autoGain"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::BOOL>("autoGain", false, {}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("autoGain"));
		moduleNode.updateReadOnly<dv::CfgType::BOOL>("autoGain", false);
	}

	if (isExposureAvailable) {
		double gainBoundsMin;
		double gainBoundsMax;

		arv_camera_get_gain_bounds(camera, &gainBoundsMin, &gainBoundsMax, &error);
		if (error != nullptr) {
			gainBoundsMin = 0.0;
			gainBoundsMax = 100.0;

			log.error.format("arv_camera_get_gain_bounds failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		double gainCurrent = arv_camera_get_gain(camera, &error);
		if (error != nullptr) {
			gainCurrent = 0.0;

			log.error.format("arv_camera_get_gain failed, error {} (code {}).", error->message, error->code);
			g_clear_error(&error);
		}

		gainCurrent = std::clamp(gainCurrent, gainBoundsMin, gainBoundsMax);

		// Reset controls to enabled and set proper range.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("gain", gainCurrent, {gainBoundsMin, gainBoundsMax},
			dv::CfgFlags::NORMAL, moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("gain"));
	}
	else {
		// Disable controls.
		moduleNode.createAttribute<dv::CfgType::DOUBLE>("gain", 0.0, {0.0, 100.0}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("gain"));
	}

	// Get available pixel formats to determine color mode support.
	guint numPixelFormats = 0;
	auto formats          = arv_camera_dup_available_pixel_formats(camera, &numPixelFormats, &error);

	if (error != nullptr) {
		int code            = error->code;
		std::string message = error->message;
		g_clear_error(&error);

		throw std::runtime_error(
			fmt::format("arv_camera_dup_available_pixel_formats failed, error {} (code {}).", message, code));
	}

	if ((numPixelFormats == 0) || (formats == nullptr)) {
		throw std::runtime_error("Could not find any valid pixel formats.");
	}

	// Go through pixel formats and search for ones directly compatible with DV's frame output.
	bool isMono8Available = false;
	bool isBGR8Available  = false;

	for (guint i = 0; i < numPixelFormats; i++) {
		if (formats[i] == ARV_PIXEL_FORMAT_MONO_8) {
			isMono8Available = true;
		}

		if (formats[i] == ARV_PIXEL_FORMAT_BGR_8_PACKED) {
			isBGR8Available = true;
		}
	}

	g_free(static_cast<gpointer>(formats));

	// No compatible formats found at all.
	if ((!isMono8Available) && (!isBGR8Available)) {
		throw std::runtime_error("No compatible pixel formats available.");
	}

	// Setup color mode selection configuration attribute.
	if (isMono8Available && (!isBGR8Available)) {
		// Only gray-scale available.
		moduleNode.createAttribute<dv::CfgType::BOOL>("colorMode", false, {}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("colorMode"));
		moduleNode.updateReadOnly<dv::CfgType::BOOL>("colorMode", false);
	}
	else if ((!isMono8Available) && isBGR8Available) {
		// Only color available.
		moduleNode.createAttribute<dv::CfgType::BOOL>("colorMode", true, {}, dv::CfgFlags::READ_ONLY,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("colorMode"));
		moduleNode.updateReadOnly<dv::CfgType::BOOL>("colorMode", true);
	}
	else {
		// Both gray-scale and color modes available.
		moduleNode.createAttribute<dv::CfgType::BOOL>("colorMode", true, {}, dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("colorMode"));
	}

	// Update configuration before starting camera to set proper payload size.
	applyConfiguration();

	// Create video stream to get frames from camera.
#if ARAVIS_CHECK_VERSION(0, 9, 0)
	stream = arv_camera_create_stream(camera, nullptr, nullptr, nullptr, &error);
#else
	stream = arv_camera_create_stream(camera, nullptr, nullptr, &error);
#endif
	if (error != nullptr) {
		int code            = error->code;
		std::string message = error->message;
		g_clear_error(&error);

		throw std::runtime_error(fmt::format("arv_camera_create_stream failed, error {} (code {}).", message, code));
	}

	if ((stream == nullptr) || (!static_cast<bool>(ARV_IS_STREAM(static_cast<gpointer>(stream))))) {
		throw std::runtime_error("Could not initialize video stream.");
	}

	// Run camera.
	arv_camera_set_acquisition_mode(camera, ARV_ACQUISITION_MODE_CONTINUOUS, nullptr);
	startAcquisition();
}

void MvCamera::run() {
	// Try to get the next available buffer.
	auto buffer = arv_stream_timeout_pop_buffer(stream, 10000); // In µs, wait for 10 ms.
	if ((buffer == nullptr) || (!static_cast<bool>(ARV_IS_BUFFER(static_cast<gpointer>(buffer))))) {
		return;
	}

	// If the buffer has a failure state, we log it, put it back and don't process it further.
	auto status = arv_buffer_get_status(buffer);
	if (status != ARV_BUFFER_STATUS_SUCCESS) {
		log.error.format("Buffer status failure, code: {}.", dv::EnumAsInteger(status));
		arv_stream_push_buffer(stream, buffer);
		return;
	}

	// If the buffer has an incorrect type, we log it, put it back and don't process it further.
	auto payloadType = arv_buffer_get_payload_type(buffer);
	if (payloadType != ARV_BUFFER_PAYLOAD_TYPE_IMAGE) {
		log.error.format("Buffer payload type incorrect, code: {}.", dv::EnumAsInteger(payloadType));
		arv_stream_push_buffer(stream, buffer);
		return;
	}

	// Buffer is fine, extract data from it.
	gint offsetX;
	gint offsetY;
	gint width;
	gint height;

	arv_buffer_get_image_region(buffer, &offsetX, &offsetY, &width, &height);

	auto timestamp = arv_buffer_get_system_timestamp(buffer) / 1000; // Timestamp in ns -> µs in DV.

	auto pixelFormat = arv_buffer_get_image_pixel_format(buffer);

	size_t pixelsLength = 0;
	auto pixels         = static_cast<const uint8_t *>(arv_buffer_get_data(buffer, &pixelsLength));

	log.debug.format("pixelsLength: {}, pixelFormat: {}, timestamp: {}, ROI: {} - {} - {} - {}.", pixelsLength,
		pixelFormat, timestamp, offsetX, offsetY, width, height);

	// Create DV frame out of collected data.
	auto outFrame = outputs.getFrameOutput("frames").frame();

	outFrame.setPosition(static_cast<int16_t>(offsetX), static_cast<int16_t>(offsetY));
	outFrame.setTimestamp(static_cast<int64_t>(timestamp));
	outFrame.setExposure(dv::Duration{static_cast<int64_t>(arv_camera_get_exposure_time(camera, nullptr))});
	outFrame.setSource(dv::FrameSource::SENSOR);

	// cv::Mat wrapping pixels array from camera.
	cv::Mat pixelsMat{
		height, width, (pixelFormat == ARV_PIXEL_FORMAT_MONO_8) ? (CV_8UC1) : (CV_8UC3), const_cast<uint8_t *>(pixels)};

	outFrame.setMat(pixelsMat); // Force copy.

	outFrame.commit();

	// Done, put buffer back in queue for next images.
	arv_stream_push_buffer(stream, buffer);
}

void MvCamera::configUpdate() {
	stopAcquisition();

	applyConfiguration();

	startAcquisition(true);
}

void MvCamera::startAcquisition(bool startThread) {
#if !ARAVIS_CHECK_VERSION(0, 9, 0)
	// Re-start the stream handling thread if requested.
	if (startThread) {
		arv_stream_start_thread(stream);
	}
#endif

	// Add buffers for images to stream. Use two buffers so that a new
	// one is ready right after getting a previous one at least.
	auto payloadSize = arv_camera_get_payload(camera, nullptr);
	log.debug.format("Payload size: {}.", payloadSize);

	for (size_t i = 0; i < STREAM_BUFFER_NUMBER; i++) {
		addNewBufferToStream(stream, payloadSize);
	}

	// Run camera.
	arv_camera_start_acquisition(camera, nullptr);
}

void MvCamera::stopAcquisition() {
	// Stop camera.
	arv_camera_stop_acquisition(camera, nullptr);

#if !ARAVIS_CHECK_VERSION(0, 9, 0)
	// Stop stream handling thread, freeing all buffers.
	arv_stream_stop_thread(stream, true);
#endif
}

void MvCamera::applyConfiguration() {
	// First color-mode, binning and region, as they influence frame-rate range.
	if (config.getBool("colorMode")) {
		arv_camera_set_pixel_format(camera, ARV_PIXEL_FORMAT_BGR_8_PACKED, nullptr);
	}
	else {
		arv_camera_set_pixel_format(camera, ARV_PIXEL_FORMAT_MONO_8, nullptr);
	}

	// Binning before region as it influences ROI sizes internally.
	if (isBinningAvailable) {
		arv_camera_set_binning(camera, config.getInt("binningX"), config.getInt("binningY"), nullptr);
	}

	arv_camera_set_region(camera, config.getInt("roiOffsetX"), config.getInt("roiOffsetY"), config.getInt("roiWidth"),
		config.getInt("roiHeight"), nullptr);

	// Update frame-rate range too, as it changes depending on settings above.
	if (isFrameRateAvailable) {
		double frameRateMin = 1.0;
		double frameRateMax = 1000.0;

		arv_camera_get_frame_rate_bounds(camera, &frameRateMin, &frameRateMax, nullptr);

		auto frameRateCurrent = config.getDouble("frameRate");

		frameRateCurrent = std::clamp(frameRateCurrent, frameRateMin, frameRateMax);

		arv_camera_set_frame_rate(camera, frameRateCurrent, nullptr);

		moduleNode.createAttribute<dv::CfgType::DOUBLE>("frameRate", frameRateCurrent, {frameRateMin, frameRateMax},
			dv::CfgFlags::NORMAL, moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("frameRate"));
	}

	if (isAutoExposureAvailable) {
		if (config.getBool("autoExposure")) {
			if (isExposureAvailable) {
				// Disable manual control.
				moduleNode.createAttribute<dv::CfgType::DOUBLE>("exposure",
					moduleNode.get<dv::CfgType::DOUBLE>("exposure"),
					moduleNode.getAttributeRanges<dv::CfgType::DOUBLE>("exposure"), dv::CfgFlags::READ_ONLY,
					moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("exposure"));
			}

			arv_camera_set_exposure_time_auto(camera, ARV_AUTO_CONTINUOUS, nullptr);
		}
		else {
			arv_camera_set_exposure_time_auto(camera, ARV_AUTO_OFF, nullptr);

			if (isExposureAvailable) {
				// Re-enable manual control. Update to last value from automatic control.
				moduleNode.createAttribute<dv::CfgType::DOUBLE>("exposure",
					arv_camera_get_exposure_time(camera, nullptr),
					moduleNode.getAttributeRanges<dv::CfgType::DOUBLE>("exposure"), dv::CfgFlags::NORMAL,
					moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("exposure"));
			}
		}
	}

	if (isExposureAvailable) {
		if ((!isAutoExposureAvailable) || (!config.getBool("autoExposure"))) {
			arv_camera_set_exposure_time(camera, config.getDouble("exposure"), nullptr);
		}
	}

	if (isAutoGainAvailable) {
		if (config.getBool("autoGain")) {
			if (isGainAvailable) {
				// Disable manual control.
				moduleNode.createAttribute<dv::CfgType::DOUBLE>("gain", moduleNode.get<dv::CfgType::DOUBLE>("gain"),
					moduleNode.getAttributeRanges<dv::CfgType::DOUBLE>("gain"), dv::CfgFlags::READ_ONLY,
					moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("gain"));
			}

			arv_camera_set_gain_auto(camera, ARV_AUTO_CONTINUOUS, nullptr);
		}
		else {
			arv_camera_set_gain_auto(camera, ARV_AUTO_OFF, nullptr);

			if (isGainAvailable) {
				// Re-enable manual control. Update to last value from automatic control.
				moduleNode.createAttribute<dv::CfgType::DOUBLE>("gain", arv_camera_get_gain(camera, nullptr),
					moduleNode.getAttributeRanges<dv::CfgType::DOUBLE>("gain"), dv::CfgFlags::NORMAL,
					moduleNode.getAttributeDescription<dv::CfgType::DOUBLE>("gain"));
			}
		}
	}

	if (isGainAvailable) {
		if ((!isAutoGainAvailable) || (!config.getBool("autoGain"))) {
			arv_camera_set_gain(camera, config.getDouble("gain"), nullptr);
		}
	}
}

MvCamera::~MvCamera() {
	stopAcquisition();

	g_object_unref(static_cast<gpointer>(stream));
	g_object_unref(static_cast<gpointer>(camera));
}

void MvCamera::addNewBufferToStream(ArvStream *stream, size_t size) {
	auto buffer = arv_buffer_new_allocate(size);

	if ((buffer == nullptr) || (!static_cast<bool>(ARV_IS_BUFFER(static_cast<gpointer>(buffer))))) {
		throw std::runtime_error("Could not initialize video buffer.");
	}

	arv_stream_push_buffer(stream, buffer); // Stream will free buffer.
}
