#pragma once

#include "../../include/dv-sdk/module.hpp"

#include <boost/algorithm/string/trim.hpp>
#include <boost/tokenizer.hpp>
#include <dv-processing/io/camera/sync_camera_input_base.hpp>
#include <dv-processing/io/camera/usb_device.hpp>

namespace dv_runtime {

static dv::io::camera::USBDevice::LogLevel deviceLogLevelConvert(const dv::logLevel level) {
	switch (level) {
		case dv::logLevel::ERROR:
			return dv::io::camera::USBDevice::LogLevel::LVL_ERROR;

		case dv::logLevel::WARNING:
			return dv::io::camera::USBDevice::LogLevel::LVL_WARNING;

		case dv::logLevel::INFO:
			return dv::io::camera::USBDevice::LogLevel::LVL_INFO;

		case dv::logLevel::DEBUG:
			return dv::io::camera::USBDevice::LogLevel::LVL_DEBUG;
	}

	return dv::io::camera::USBDevice::LogLevel::LVL_NONE;
}

class TimeSync {
public:
	static void checkConfigFields(dv::Config::Node moduleNode, const dv::io::camera::SyncCameraInputBase &device) {
		// Check syncWith is properly configured.
		if (!device.isMaster()) {
			if (!moduleNode.getString("syncWith").empty()) {
				throw dv::exceptions::RuntimeError("'syncWith' field must be empty on secondary cameras.");
			}

			// Disable unused fields on secondary cameras.
			moduleNode.createAttribute<dv::CfgType::STRING>("syncWith",
				dv::Config::AttributeValue<dv::Config::AttributeType::STRING>(
					moduleNode.get<dv::CfgType::STRING>("syncWith")),
				moduleNode.getAttributeRanges<dv::CfgType::STRING>("syncWith"),
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
				moduleNode.getAttributeDescription<dv::CfgType::STRING>("syncWith"));

			moduleNode.createAttribute<dv::CfgType::BOOL>("syncTime", moduleNode.get<dv::CfgType::BOOL>("syncTime"),
				moduleNode.getAttributeRanges<dv::CfgType::BOOL>("syncTime"),
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
				moduleNode.getAttributeDescription<dv::CfgType::BOOL>("syncTime"));
		}
	}

	static void resetConfigFields(dv::Config::Node moduleNode) {
		// Reset fields to default.
		moduleNode.createAttribute<dv::CfgType::STRING>("syncWith",
			dv::Config::AttributeValue<dv::Config::AttributeType::STRING>(
				moduleNode.get<dv::CfgType::STRING>("syncWith")),
			moduleNode.getAttributeRanges<dv::CfgType::STRING>("syncWith"), dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::STRING>("syncWith"));

		moduleNode.createAttribute<dv::CfgType::BOOL>("syncTime", moduleNode.get<dv::CfgType::BOOL>("syncTime"),
			moduleNode.getAttributeRanges<dv::CfgType::BOOL>("syncTime"), dv::CfgFlags::NORMAL,
			moduleNode.getAttributeDescription<dv::CfgType::BOOL>("syncTime"));
	}

	static void offsetNotify(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *syncOffsetUpdated = static_cast<std::atomic_bool *>(userData);
		const std::string key{changeKey};

		if (key == "tsOffset") {
			*syncOffsetUpdated = true;
		}
	}

	static void execute(dv::Config::Node moduleNode, dv::io::camera::SyncCameraInputBase &device,
		std::atomic_bool &syncOffsetUpdated, dv::Logger &log) {
		if (moduleNode.getBool("syncTime")) {
			if (device.isMaster()) {
				// Master clock camera.
				moduleNode.putBool("syncTime", false); // Reset button.

				// Parse field with module names.
				auto syncWithField = moduleNode.getString("syncWith");

				boost::trim(syncWithField);

				if (syncWithField.empty()) {
					log.error("'syncWith' field must contain the module name(s) of all the secondary cameras, "
							  "comma-separated.");
					return;
				}

				boost::tokenizer<boost::char_separator<char>> syncWithFieldNames(
					syncWithField, boost::char_separator<char>(",", nullptr));

				std::vector<std::string> secondaryNames{};

				for (auto cameraName : syncWithFieldNames) {
					boost::trim(cameraName);

					if (cameraName.empty()) {
						log.error("Empty module name in 'syncWith' field.");
						return;
					}

					if (cameraName == moduleNode.getName()) {
						log.error("Invalid module name in 'syncWith' field.");
						return;
					}

					secondaryNames.push_back(std::move(cameraName));
				}

				if (secondaryNames.empty()) {
					log.error("'syncWith' field must contain the module name(s) of all the secondary cameras, "
							  "comma-separated.");
					return;
				}

				// Check secondary cameras are running.
				for (const auto &cameraName : secondaryNames) {
					if (!moduleNode.existsRelativeNode(fmt::format("../{}/", cameraName))) {
						log.error(fmt::format(
							"Secondary camera module with name '{}' does not exist. Add it first.", cameraName));
						return;
					}

					if (!moduleNode.getRelativeNode(fmt::format("../{}/", cameraName))
							.existsAttribute("isRunning", dv::CfgType::BOOL)
						|| !moduleNode.getRelativeNode(fmt::format("../{}/", cameraName)).getBool("isRunning")) {
						log.error(fmt::format(
							"Secondary camera module with name '{}' is not running. Start it first.", cameraName));
						return;
					}

					if (!moduleNode.getRelativeNode(fmt::format("../{}/", cameraName))
							.existsAttribute("syncTime", dv::CfgType::BOOL)
						|| !moduleNode.existsRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
						|| !moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							.existsAttribute("tsOffset", dv::CfgType::LONG)
						|| !moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							.existsAttribute("syncStatus", dv::CfgType::STRING)) {
						log.error(fmt::format(
							"Secondary camera module with name '{}' is invalid, no time sync possible. Wrong name?",
							cameraName));
						return;
					}

					if (!moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							.existsAttribute("deviceIsMaster", dv::CfgType::BOOL)
						|| moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							.getBool("deviceIsMaster")) {
						log.error(fmt::format("Secondary camera module with name '{}' is a master clock device, no "
											  "time sync possible. Sync cable disconnected?",
							cameraName));
						return;
					}
				}

				// Trigger secondary cameras to enter configUpdate() too and select the secondary camera execution
				// path for the 'syncTime' button.
				for (const auto &cameraName : secondaryNames) {
					moduleNode.getRelativeNode(fmt::format("../{}/", cameraName))
						.updateReadOnly<dv::CfgType::BOOL>("syncTime", true);
				}

				// Wait for them to have reset the time tracking to WAIT_RESET state.
				auto startTime = dv::now();
				while (std::any_of(secondaryNames.begin(), secondaryNames.end(), [moduleNode](auto &cameraName) {
					return moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							   .getString("syncStatus")
						!= "WAIT_RESET";
				})) {
					std::this_thread::sleep_for(std::chrono::milliseconds{1});

					if ((dv::now() - startTime) > dv::io::camera::SyncCameraInputBase::TIME_SYNC_TIMEOUT.count()) {
						log.error("Time synchronization timeout reached: secondary cameras no WAIT_RESET.");
						return;
					}
				}

				// Execute timestamp sync from Master camera perspective.
				moduleNode.getRelativeNode("sourceInfo/")
					.updateReadOnly<dv::CfgType::STRING>("syncStatus", "WAIT_RESET");

				try {
					// Will set mSyncState=WAIT_RESET in master clock device.
					device.sendTimestampReset();

					// Will wait on master.
					device.waitForTimestampReset();
				}
				catch (const dv::exceptions::Exception &e) {
					log.error(e.shortWhat());
					return;
				}

				// Got reset, publish that and the new offset.
				moduleNode.getRelativeNode("sourceInfo/")
					.updateReadOnly<dv::CfgType::STRING>("syncStatus", "GOT_RESET");

				moduleNode.getRelativeNode("sourceInfo/")
					.updateReadOnly<dv::CfgType::LONG>("tsOffset", device.getTimestampOffset().count());

				// Wait for all secondary cameras to also have seen the reset.
				startTime = dv::now();
				while (std::any_of(secondaryNames.begin(), secondaryNames.end(), [moduleNode](auto &cameraName) {
					return moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
							   .getString("syncStatus")
						!= "GOT_RESET";
				})) {
					std::this_thread::sleep_for(std::chrono::milliseconds{1});

					if ((dv::now() - startTime) > dv::io::camera::SyncCameraInputBase::TIME_SYNC_TIMEOUT.count()) {
						log.error("Time synchronization timeout reached: secondary cameras no GOT_RESET.");
						return;
					}
				}

				// Update all secondary cameras' offsets with a new value.
				for (const auto &cameraName : secondaryNames) {
					moduleNode.getRelativeNode(fmt::format("../{}/sourceInfo/", cameraName))
						.updateReadOnly<dv::CfgType::LONG>("tsOffset", device.getTimestampOffset().count());
				}
			}
			else {
				// Secondary camera.
				moduleNode.updateReadOnly<dv::CfgType::BOOL>("syncTime", false); // Reset button.

				// Execute timestamp sync from secondary camera perspective.
				device.mSyncState = dv::io::camera::SyncCameraInputBase::SyncState::WAIT_RESET;

				moduleNode.getRelativeNode("sourceInfo/")
					.updateReadOnly<dv::CfgType::STRING>("syncStatus", "WAIT_RESET");

				try {
					// Will wait on secondary camera.
					device.waitForTimestampReset();
				}
				catch (const dv::exceptions::Exception &e) {
					log.error(e.shortWhat());
					return;
				}

				moduleNode.getRelativeNode("sourceInfo/")
					.updateReadOnly<dv::CfgType::STRING>("syncStatus", "GOT_RESET");

				// Wait on tsOffset to be updated by Master device.
				const auto startTime = dv::now();
				while (!syncOffsetUpdated) {
					std::this_thread::sleep_for(std::chrono::milliseconds{1});

					if ((dv::now() - startTime) > dv::io::camera::SyncCameraInputBase::TIME_SYNC_TIMEOUT.count()) {
						log.error("Time synchronization timeout reached: offset update.");
						return;
					}
				}
				syncOffsetUpdated = false; // Reset.

				const auto newOffset = moduleNode.getRelativeNode("sourceInfo/").getLong("tsOffset");

				device.setTimestampOffset(std::chrono::microseconds{newOffset});
			}

			// Sync successful!
			device.mSyncState = dv::io::camera::SyncCameraInputBase::SyncState::SYNC_OK;

			moduleNode.getRelativeNode("sourceInfo/").updateReadOnly<dv::CfgType::STRING>("syncStatus", "SYNC_OK");

			log.info.format("Sync successful, offset is {}.", device.getTimestampOffset());
		}
	}
};

} // namespace dv_runtime
