#include "../../include/dv-sdk/data/event.hpp"
#include "../../include/dv-sdk/data/trigger.hpp"

#include "../../src/log.hpp"
#include "common.hpp"

#include <dv-processing/io/camera/dvs128.hpp>

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

	// Get current device status.
	const auto devices = dv::io::camera::DVS128::findDevices();

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

	for (const auto &dev : devices) {
		const auto nodeName
			= fmt::format("dvs128_{:d}-{:d}/", static_cast<int>(dev.busNumber), static_cast<int>(dev.devAddress));

		auto devNode = devicesNode.getRelativeNode(nodeName);

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

		devNode.create<dv::CfgType::INT>("USBBusNumber", dev.busNumber, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB bus number.");
		devNode.create<dv::CfgType::INT>("USBDeviceAddress", dev.devAddress, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB device address.");

		devNode.create<dv::CfgType::STRING>("SerialNumber", dev.serialNumber, {0, 8},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB device serial number.");
		devNode.create<dv::CfgType::INT>("FirmwareVersion", dev.firmwareVersion, {0, INT16_MAX},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Version of device firmware.");
	}

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

namespace dv_runtime {

class DVS128 final : public dv::ModuleBase {
private:
	dv::io::camera::DVS128 mDevice;
	std::mutex mConfigLock{};
	std::atomic_bool mSyncOffsetUpdated{false};

public:
	static void initOutputs(dv::OutputDefinitionList &out) {
		out.addEventOutput("events");
		out.addTriggerOutput("triggers");
	}

	static const char *initDescription() {
		return "iniVation DVS128 camera support.";
	}

	static void initConfigOptions(dv::RuntimeConfig &config) {
		config.add("serialNumber", dv::ConfigOption::stringOption("USB serial number restriction.", ""));

		config.add("syncWith",
			dv::ConfigOption::stringOption(
				"Module name(s) of all the secondary cameras to sync timestamps with (comma-separated).", ""));
		config.add("syncTime",
			dv::ConfigOption::buttonOption("Synchronize timestamps with secondary cameras.", "Synchronize timestamps"));

		configEventsCreate(config);
		configSystemCreate(config);
		configBiasCreate(config);
	}

	DVS128() :
		mDevice(
			config.getString("serialNumber"),
			[this](const dv::io::camera::USBDevice::LogLevel level, const std::string_view deviceName,
				const std::string_view message) {
				switch (level) {
					case dv::io::camera::USBDevice::LogLevel::LVL_ERROR:
						log.error(fmt::format("{}: {}", deviceName, message));
						break;

					case dv::io::camera::USBDevice::LogLevel::LVL_WARNING:
						log.warning(fmt::format("{}: {}", deviceName, message));
						break;

					case dv::io::camera::USBDevice::LogLevel::LVL_INFO:
						log.info(fmt::format("{}: {}", deviceName, message));
						break;

					default:
					case dv::io::camera::USBDevice::LogLevel::LVL_DEBUG:
						log.debug(fmt::format("{}: {}", deviceName, message));
						break;
				}
			},
			deviceLogLevelConvert(dv::LoggerInternal::logLevelNameToEnum(config.getString("logLevel")))) {
		// Setup outputs.
		outputs.getEventOutput("events").setup(128, 128, mDevice.getCameraName());
		outputs.getTriggerOutput("triggers").setup(mDevice.getCameraName());

		TimeSync::checkConfigFields(moduleNode, mDevice);

		auto sourceInfoNode = moduleNode.getRelativeNode("sourceInfo/");

		sourceInfoNode.create<dv::CfgType::STRING>("serialNumber", mDevice.getSerialNumber(), {0, 8},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device serial number.");
		sourceInfoNode.create<dv::CfgType::INT>("usbBusNumber", mDevice.getDeviceDescriptor().busNumber, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB bus number.");
		sourceInfoNode.create<dv::CfgType::INT>("usbDeviceAddress", mDevice.getDeviceDescriptor().devAddress, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB device address.");

		sourceInfoNode.create<dv::CfgType::INT>("firmwareVersion", mDevice.getFirmwareVersion(),
			{mDevice.getFirmwareVersion(), mDevice.getFirmwareVersion()},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB firmware version.");

		sourceInfoNode.create<dv::CfgType::STRING>("source", mDevice.getCameraName(),
			{static_cast<int32_t>(mDevice.getCameraName().length()),
				static_cast<int32_t>(mDevice.getCameraName().length())},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device source information.");

		// Default time sync state after startup.
		sourceInfoNode.create<dv::CfgType::STRING>("syncStatus", "NO_SYNC", {1, 10},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Time synchronization status.");

		sourceInfoNode.create<dv::CfgType::LONG>("tsOffset", mDevice.getTimestampOffset().count(), {0, INT64_MAX},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Time offset of data stream starting point to Unix time in µs.");

		sourceInfoNode.create<dv::CfgType::BOOL>("deviceIsMaster", mDevice.isMaster(), {},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Timestamp synchronization support: device master status.");

		// Initial configuration, since we don't use configUpdate() here.
		configSystem();
		moduleNode.getRelativeNode("system/").addAttributeListener(this, &configSystemTrampoline);

		configBias();
		moduleNode.getRelativeNode("bias/").addAttributeListener(this, &configBiasTrampoline);

		configEvents();
		moduleNode.getRelativeNode("events/").addAttributeListener(this, &configEventsTrampoline);

		moduleNode.getRelativeNode("sourceInfo/").addAttributeListener(&mSyncOffsetUpdated, &TimeSync::offsetNotify);
	}

	~DVS128() override {
		moduleNode.getRelativeNode("system/").removeAttributeListener(this, &configSystemTrampoline);

		moduleNode.getRelativeNode("bias/").removeAttributeListener(this, &configBiasTrampoline);

		moduleNode.getRelativeNode("events/").removeAttributeListener(this, &configEventsTrampoline);

		moduleNode.getRelativeNode("sourceInfo/").removeAttributeListener(&mSyncOffsetUpdated, &TimeSync::offsetNotify);

		// Data capture stopped already in respective destructors.
		// Clear sourceInfo node.
		auto sourceInfoNode = moduleNode.getRelativeNode("sourceInfo/");
		sourceInfoNode.removeAllAttributes();

		TimeSync::resetConfigFields(moduleNode);
	}

	void run() override {
		if (!mDevice.isRunning()) {
			// Device gone, stop module.
			config.setBool("running", false);
			return;
		}

		bool gotData{false};

		auto events = mDevice.getNextEventPacket();

		if (events) {
			gotData = true;

			// Send data out.
			auto eventsOutput              = outputs.getEventOutput("events").events();
			*eventsOutput.getBasePointer() = std::move(*events);
			eventsOutput.commit();
		}

		auto triggers = mDevice.getNextTriggerBatch();

		if (triggers) {
			gotData = true;

			// Send data out.
			auto triggersOutput                       = outputs.getTriggerOutput("triggers").data();
			triggersOutput.getBasePointer()->elements = std::move(*triggers);
			triggersOutput.commit();
		}

		// No data at all ready during this pass, sleep a bit to reduce load.
		if (!gotData) {
			std::this_thread::sleep_for(std::chrono::microseconds{500});
		}
	}

	void configUpdate() override {
		std::scoped_lock lock(mConfigLock);

		mDevice.setLogLevel(
			deviceLogLevelConvert(dv::LoggerInternal::logLevelNameToEnum(config.getString("logLevel"))));

		// Last update position.
		TimeSync::execute(moduleNode, mDevice, mSyncOffsetUpdated, log);
	}

private:
	static void configEventsCreate(dv::RuntimeConfig &config) {
		config.add("events/flatten",
			dv::ConfigOption::listOption("Flatten events control.", 0, {"none", "all ON", "ON only", "OFF only"}));

		config.add("events/flipHorizontal",
			dv::ConfigOption::boolOption("Flip event coordinates along horizontal X axis.", false));
		config.add("events/flipVertical",
			dv::ConfigOption::boolOption("Flip event coordinates along vertical Y axis.", false));

		config.add(
			"events/cropStartX", dv::ConfigOption::intOption("Horizontal (X axis) start of ROI.", 0, 0, 128 - 1));
		config.add("events/cropStartY", dv::ConfigOption::intOption("Vertical (Y axis) start of ROI.", 0, 0, 128 - 1));
		config.add("events/cropWidth", dv::ConfigOption::intOption("ROI width.", 128, 1, 128));
		config.add("events/cropHeight", dv::ConfigOption::intOption("ROI height.", 128, 1, 128));

		config.setPriorityOptions({"events/"});
	}

	static dv::io::camera::CameraInputBase::Flatten mapFlatten(const std::string &strVal) {
		if (strVal == "all ON") {
			return dv::io::camera::CameraInputBase::Flatten::FLATTEN_ON;
		}
		else if (strVal == "ON only") {
			return dv::io::camera::CameraInputBase::Flatten::ON_ONLY;
		}
		else if (strVal == "OFF only") {
			return dv::io::camera::CameraInputBase::Flatten::OFF_ONLY;
		}
		else {
			// Default: no flattening.
			return dv::io::camera::CameraInputBase::Flatten::NONE;
		}
	}

	static void configEventsTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DVS128 *>(userData);

		obj->configEvents();
	}

	void configEvents() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		const auto flatten = config.getString("events/flatten");
		mDevice.setFlatten(mapFlatten(flatten));

		mDevice.setFlipHorizontal(config.getBool("events/flipHorizontal"));
		mDevice.setFlipVertical(config.getBool("events/flipVertical"));

		try {
			mDevice.setCropArea(cv::Rect{config.getInt("events/cropStartX"), config.getInt("events/cropStartY"),
				config.getInt("events/cropWidth"), config.getInt("events/cropHeight")});
		}
		catch (const std::exception &) {
			log.warning("Invalid events crop area setting, verify that (cropStartX + cropWidth) is smaller than sensor "
						"width and that (cropStartY + cropHeight) is smaller than sensor height.");
		}
	}

	static void configBiasCreate(dv::RuntimeConfig &config) {
		config.add("bias/cas", dv::ConfigOption::intOption("Photoreceptor cascode.", 1992, 0, (0x01 << 24) - 1));
		config.add(
			"bias/injGnd", dv::ConfigOption::intOption("Differentiator switch level.", 1108364, 0, (0x01 << 24) - 1));
		config.add("bias/reqPd", dv::ConfigOption::intOption("AER request pull-down.", 16777215, 0, (0x01 << 24) - 1));
		config.add(
			"bias/puX", dv::ConfigOption::intOption("2nd dimension AER static pull-up.", 8159221, 0, (0x01 << 24) - 1));
		config.add("bias/diffOff",
			dv::ConfigOption::intOption("OFF threshold - lower to raise threshold.", 132, 0, (0x01 << 24) - 1));
		config.add("bias/req", dv::ConfigOption::intOption("OFF request inverter bias.", 309590, 0, (0x01 << 24) - 1));
		config.add("bias/refr", dv::ConfigOption::intOption("Refractory period.", 969, 0, (0x01 << 24) - 1));
		config.add("bias/puY",
			dv::ConfigOption::intOption("1st dimension AER static pull-up.", 16777215, 0, (0x01 << 24) - 1));
		config.add("bias/diffOn",
			dv::ConfigOption::intOption("ON threshold - higher to raise threshold.", 209996, 0, (0x01 << 24) - 1));
		config.add("bias/diff", dv::ConfigOption::intOption("Differentiator.", 13125, 0, (0x01 << 24) - 1));
		config.add(
			"bias/foll", dv::ConfigOption::intOption("Source follower buffer between photoreceptor and differentiator.",
							 271, 0, (0x01 << 24) - 1));
		config.add("bias/pr", dv::ConfigOption::intOption("Photoreceptor.", 217, 0, (0x01 << 24) - 1));

		config.setPriorityOptions({"bias/diffOn", "bias/diffOff"});
	}

	static void configBiasTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DVS128 *>(userData);

		obj->configBias();
	}

	void configBias() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		mDevice.setBias(dv::io::camera::DVS128::Bias::CAS, static_cast<uint32_t>(config.getInt("bias/cas")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::INJGND, static_cast<uint32_t>(config.getInt("bias/injGnd")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::REQPD, static_cast<uint32_t>(config.getInt("bias/reqPd")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::PUX, static_cast<uint32_t>(config.getInt("bias/puX")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::DIFF_OFF, static_cast<uint32_t>(config.getInt("bias/diffOff")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::REQ, static_cast<uint32_t>(config.getInt("bias/req")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::REFR, static_cast<uint32_t>(config.getInt("bias/refr")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::PUY, static_cast<uint32_t>(config.getInt("bias/puY")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::DIFF_ON, static_cast<uint32_t>(config.getInt("bias/diffOn")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::DIFF, static_cast<uint32_t>(config.getInt("bias/diff")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::FOLL, static_cast<uint32_t>(config.getInt("bias/foll")));
		mDevice.setBias(dv::io::camera::DVS128::Bias::PR, static_cast<uint32_t>(config.getInt("bias/pr")));
	}

	static void configSystemCreate(dv::RuntimeConfig &config) {
		config.add("system/setMaster",
			dv::ConfigOption::boolOption(
				"Control clock master functionality of this camera. By default this is enabled and allows the camera "
				"to receive triggers on its IN pin. If disabled, the camera will expect a clock synchronization signal "
				"on its IN pin and act as a secondary camera.",
				true));

		// USB buffer settings.
		config.add("system/usbTransfersNumber", dv::ConfigOption::intOption("Number of USB transfers.", 32, 2, 128));
		config.add("system/usbTransfersSize",
			dv::ConfigOption::intOption("Size in bytes of data buffers for USB transfers.", 8 * 1024, 512, 64 * 1024));

		config.add(
			"system/timeInterval", dv::ConfigOption::intOption("Time interval in µs for data parsing and forwarding.",
									   10'000, 1, 120 * 1000 * 1000));

		config.setPriorityOptions({"system/"});
	}

	static void configSystemTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DVS128 *>(userData);

		obj->configSystem();
	}

	void configSystem() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		mDevice.setMaster(config.getBool("system/setMaster"));

		mDevice.setTransfersNumber(config.getInt("system/usbTransfersNumber"));
		mDevice.setTransfersSize(config.getInt("system/usbTransfersSize"));

		mDevice.setTimeInterval(std::chrono::microseconds{config.getInt("system/timeInterval")});
	}
};

} // namespace dv_runtime

registerModuleClass(dv_runtime::DVS128)
