#pragma once

#include "camera_input_base.hpp"

// DV-ROS synchronization support.
namespace dv_capture_node {
class CaptureNode;
} // namespace dv_capture_node

// DV-runtime synchronization support.
namespace dv_runtime {
class TimeSync;
} // namespace dv_runtime

namespace dv::io::camera {

/**
 * Camera input base class to abstract live camera and recorded files with a common interface.
 */
class SyncCameraInputBase : public CameraInputBase {
public:
	/**
	 * Report if this camera is a clock synchronization master.
	 * @return true if clock master, false otherwise.
	 */
	[[nodiscard]] virtual bool isMaster() const = 0;

	/**
	 * Report if this camera is properly synchronized with others in a multi-camera scenario.
	 * @return the camera is synchronized with others.
	 */
	[[nodiscard]] bool isSynchronized() const {
		return mSyncState == SyncState::SYNC_OK;
	}

	/**
	 * Synchronize this camera with another. This camera is expected to be the clock synchronization master.
	 * @param secondary secondary camera.
	 */
	void synchronizeWith(const std::unique_ptr<SyncCameraInputBase> &secondary) {
		synchronizeWith(secondary.get());
	}

	/**
	 * Synchronize this camera with another. This camera is expected to be the clock synchronization master.
	 * @param secondary secondary camera.
	 */
	void synchronizeWith(SyncCameraInputBase &secondary) {
		synchronizeWith(&secondary);
	}

	/**
	 * Synchronize this camera with another. This camera is expected to be the clock synchronization master.
	 * @param secondary secondary camera.
	 */
	void synchronizeWith(SyncCameraInputBase *secondary) {
		std::array<SyncCameraInputBase *, 1> secondaryCameras{secondary};

		synchronizeWith(secondaryCameras);
	}

	/**
	 * Synchronize this camera with multiple others. This camera is expected to be the clock synchronization master.
	 * @param secondaryCameras secondary cameras (one or more).
	 */
	void synchronizeWith(const std::span<SyncCameraInputBase *> secondaryCameras) {
		if (!isMaster()) {
			throw dv::exceptions::RuntimeError("Primary camera is not a clock master.");
		}

		if (secondaryCameras.empty()) {
			throw dv::exceptions::RuntimeError("No secondary cameras given.");
		}

		if (std::ranges::find(secondaryCameras, this) != secondaryCameras.end()) {
			throw dv::exceptions::RuntimeError("Cannot synchronize camera with itself.");
		}

		if (std::ranges::find(secondaryCameras, nullptr) != secondaryCameras.end()) {
			throw dv::exceptions::RuntimeError("Secondary cameras must all be defined.");
		}

		for (auto secondary : secondaryCameras) {
			if (secondary->isMaster()) {
				throw dv::exceptions::RuntimeError(
					"Secondary camera is a clock master. Only the primary can be a master!");
			}
		}

		for (auto secondary : secondaryCameras) {
			secondary->mSyncState = SyncState::WAIT_RESET;
		}
		sendTimestampReset(); // Will set mSyncState=WAIT_RESET in master clock device.

		waitForTimestampReset(); // Will wait on master, secondary cameras are waited on below.

		const auto startTime = dv::now();
		while (std::any_of(secondaryCameras.begin(), secondaryCameras.end(), [](auto *secondary) {
			return !secondary->gotReset();
		})) {
			std::this_thread::sleep_for(std::chrono::milliseconds{1});

			if ((dv::now() - startTime) > TIME_SYNC_TIMEOUT.count()) {
				throw dv::exceptions::RuntimeError("Multi-camera time synchronization timeout reached.");
			}
		}

		// Sync successful!
		mSyncState = SyncState::SYNC_OK;

		for (auto secondary : secondaryCameras) {
			secondary->setTimeInterval(getTimeInterval());
			secondary->setTimestampOffset(getTimestampOffset());
			secondary->mSyncState = SyncState::SYNC_OK;
		}
	}

protected:
	// Synchronization support, needs internal access.
	friend class dv_capture_node::CaptureNode;
	friend class dv_runtime::TimeSync;

	enum class SyncState {
		WAIT_RESET,
		GOT_RESET,
		SYNC_OK,
	};

	static constexpr std::chrono::microseconds TIME_SYNC_TIMEOUT{5'000'000};

	std::atomic<SyncState> mSyncState{SyncState::WAIT_RESET};

	/**
	 * Send a timestamp reset command to the device.
	 */
	virtual void sendTimestampReset() = 0;

	/**
	 * Set a new timestamp offset value for the camera.
	 *
	 * @param timestampOffset   New timestamp offset value in microseconds.
	 */
	virtual void setTimestampOffset(std::chrono::microseconds timestampOffset) = 0;

	/**
	 * Wait until a timestamp reset is observed from this camera.
	 */
	void waitForTimestampReset() const {
		const auto startTime = dv::now();

		while (!gotReset()) {
			std::this_thread::sleep_for(std::chrono::milliseconds{1});

			if ((dv::now() - startTime) > TIME_SYNC_TIMEOUT.count()) {
				throw dv::exceptions::RuntimeError("Camera time synchronization timeout reached.");
			}
		}
	}

	/**
	 * Report if we observed the timestamp reset from the device.
	 * @return wether we got a timestamp reset or not.
	 */
	[[nodiscard]] bool gotReset() const {
		return mSyncState == SyncState::GOT_RESET;
	}
};

using SyncCameraPtr = std::unique_ptr<SyncCameraInputBase>;

/**
 * Synchronize any number of cameras with each-other.
 * Only one can be a master clock camera.
 * @param cameras cameras to synchronize.
 */
inline void synchronizeAny(const std::span<SyncCameraInputBase *> cameras) {
	if (cameras.size() < 2) {
		throw dv::exceptions::RuntimeError("At least two cameras required for synchronization.");
	}

	// Check that there is at least one clock master camera.
	std::vector<SyncCameraInputBase *> masterCameras;
	std::vector<SyncCameraInputBase *> secondaryCameras;

	for (auto *camera : cameras) {
		if (camera != nullptr) {
			if (camera->isMaster()) {
				masterCameras.push_back(camera);
			}
			else {
				secondaryCameras.push_back(camera);
			}
		}
	}

	if (masterCameras.empty()) {
		throw dv::exceptions::RuntimeError("No clock master camera found.");
	}

	if (masterCameras.size() > 1) {
		throw dv::exceptions::RuntimeError("Multiple clock master cameras found.");
	}

	if (secondaryCameras.empty()) {
		throw dv::exceptions::RuntimeError("No secondary cameras found.");
	}

	masterCameras[0]->synchronizeWith(secondaryCameras);
}

/**
 * Synchronize two cameras with each-other.
 * Only one can be a master clock camera.
 * @param first camera to synchronize.
 * @param second camera to synchronize.
 */
inline void synchronizeAnyTwo(SyncCameraInputBase *first, SyncCameraInputBase *second) {
	std::array<SyncCameraInputBase *, 2> cameras{first, second};

	synchronizeAny(cameras);
}

/**
 * Synchronize two cameras with each-other.
 * Only one can be a master clock camera.
 * @param first camera to synchronize.
 * @param second camera to synchronize.
 */
inline void synchronizeAnyTwo(SyncCameraInputBase &first, SyncCameraInputBase &second) {
	synchronizeAnyTwo(&first, &second);
}

/**
 * Synchronize two cameras with each-other.
 * Only one can be a master clock camera.
 * @param first camera to synchronize.
 * @param second camera to synchronize.
 */
inline void synchronizeAnyTwo(const SyncCameraPtr &first, const SyncCameraPtr &second) {
	synchronizeAnyTwo(first.get(), second.get());
}

} // namespace dv::io::camera
