#pragma once

#include "../data_read_handler.hpp"
#include "../input_base.hpp"
#include "imu_support.hpp"
#include "parsers/parser.hpp"

#include <boost/circular_buffer.hpp>

namespace dv::io::camera {

/**
 * Camera input base class to abstract live camera and recorded files with a common interface.
 */
class CameraInputBase : public InputBase {
public:
	/**
	 * Event flattening modes, used to change polarity of events returned by the camera.
	 */
	enum class Flatten {
		/// No change.
		NONE = 0,
		/// All events become ON events.
		FLATTEN_ON = 1,
		// Only ON events are sent by the camera.
		ON_ONLY = 2,
		// Only OFF events are sent by the camera.
		OFF_ONLY = 3,
	};

	friend std::ostream &operator<<(std::ostream &os, const Flatten &var) {
		switch (var) {
			case Flatten::NONE:
				return os << "Original events";

			case Flatten::FLATTEN_ON:
				return os << "All events as ON events";

			case Flatten::ON_ONLY:
				return os << "Only ON events";

			case Flatten::OFF_ONLY:
				return os << "Only OFF events";
		}

		return os;
	}

	/**
	 * Status of horizontal events flip.
	 * @return status of horizontal events flip.
	 */
	[[nodiscard]] virtual bool getFlipHorizontal() const = 0;

	/**
	 * Flip events horizontally.
	 * @param flipHorizontalEvents flip events horizontally.
	 */
	virtual void setFlipHorizontal(bool flipHorizontalEvents) = 0;

	/**
	 * Status of vertical events flip.
	 * @return status of vertical events flip.
	 */
	[[nodiscard]] virtual bool getFlipVertical() const = 0;

	/**
	 * Flip events vertically.
	 * @param flipVerticalEvents flip events vertically.
	 */
	virtual void setFlipVertical(bool flipVerticalEvents) = 0;

	/**
	 * Status of event polarity flattening.
	 * @return status of event polarity flattening.
	 */
	[[nodiscard]] virtual Flatten getFlatten() const = 0;

	/**
	 * Flatten events polarity.
	 * @param flattenEvents flattening mode.
	 */
	virtual void setFlatten(Flatten flattenEvents) = 0;

	/**
	 * Get events Region of Interest (ROI).
	 * @return get events Region of Interest (ROI).
	 */
	[[nodiscard]] virtual cv::Rect getCropArea() const = 0;

	/**
	 * Set events Region of Interest (ROI). Usually hardware accelerated.
	 * @param cropAreaEvents region of interest (ROI) position and size.
	 */
	virtual void setCropArea(cv::Rect cropAreaEvents) = 0;

	/**
	 * Return IMU model used on device.
	 * @return IMU model in use.
	 */
	[[nodiscard]] virtual imu::ImuModel getImuModel() const = 0;

	/**
	 * Return pixel pitch distance for the connected camera model. The value is returned in meters.
	 * @return 	Pixel pitch distance in meters according to the connected device.
	 */
	[[nodiscard]] virtual float getPixelPitch() const = 0;

	/**
	 * Get the time interval for data commit.
	 *
	 * @return      Time interval in microseconds.
	 */
	[[nodiscard]] virtual std::chrono::microseconds getTimeInterval() const = 0;

	/**
	 * Set a new time interval value for data commit.
	 * Data is put in the queues for getNextEventBatch(), readNext(), ... at this interval's rate.
	 *
	 * @param timeInterval   New time interval value in microseconds.
	 */
	virtual void setTimeInterval(std::chrono::microseconds timeInterval) = 0;

	/**
	 * Get the timestamp offset.
	 *
	 * @return      Absolute timestamp offset value in microseconds.
	 */
	[[nodiscard]] virtual std::chrono::microseconds getTimestampOffset() const = 0;

	/**
	 * Parse and retrieve next event packet (internal format).
	 * @return 		Event packet or `std::nullopt` if no events were received since last read.
	 */
	[[nodiscard]] std::optional<dv::EventPacket> getNextEventPacket() {
		return mBuffers.popEventPacket();
	}

	/**
	 * Parse and retrieve next event batch.
	 * @return 		Event batch or `std::nullopt` if no events were received since last read.
	 */
	[[nodiscard]] std::optional<dv::EventStore> getNextEventBatch() override {
		auto eventPacket = mBuffers.popEventPacket();
		if (!eventPacket.has_value()) {
			return std::nullopt;
		}

		return dv::EventStore{std::make_shared<dv::EventPacket>(std::move(eventPacket.value()))};
	}

	/**
	 * Parse and retrieve next frame.
	 * @return 		Frame or `std::nullopt` if no frames were received since last read.
	 */
	[[nodiscard]] std::optional<dv::Frame> getNextFrame() override {
		return mBuffers.popFrame();
	}

	/**
	 * Parse and retrieve next IMU data batch.
	 * @return 		IMU data batch or `std::nullopt` if no IMU data was received since last read.
	 */
	[[nodiscard]] std::optional<std::vector<dv::IMU>> getNextImuBatch() override {
		auto imuPacket = mBuffers.popImuPacket();
		if (!imuPacket.has_value()) {
			return std::nullopt;
		}

		return imuPacket.value().elements;
	}

	/**
	 * Parse and retrieve next trigger data batch.
	 * @return 		Trigger data batch or `std::nullopt` if no triggers were received since last read.
	 */
	[[nodiscard]] std::optional<std::vector<dv::Trigger>> getNextTriggerBatch() override {
		auto triggerPacket = mBuffers.popTriggerPacket();
		if (!triggerPacket.has_value()) {
			return std::nullopt;
		}

		return triggerPacket.value().elements;
	}

	/**
	 * Check whether a stream with given name is available.
	 * @return True if data stream is available, false otherwise.
	 */
	[[nodiscard]] bool isStreamAvailable(const std::string_view streamName) const override {
		if (streamName == "events") {
			return isEventStreamAvailable();
		}
		if (streamName == "frames") {
			return isFrameStreamAvailable();
		}
		if (streamName == "imu") {
			return isImuStreamAvailable();
		}
		if (streamName == "triggers") {
			return isTriggerStreamAvailable();
		}
		return false;
	}

	/**
	 * Get latest timestamp of event data stream that has been read from the capture class.
	 * @return Latest processed event timestamp; returns -1 if no data was processed or stream is unavailable.
	 */
	[[nodiscard]] int64_t getEventSeekTime() const {
		return mBuffers.getEventSeekTime();
	}

	/**
	 * Get latest timestamp of frames stream that has been read from the capture class.
	 * @return Latest processed frame timestamp; returns -1 if no data was processed or stream is unavailable.
	 */
	[[nodiscard]] int64_t getFrameSeekTime() const {
		return mBuffers.getFrameSeekTime();
	}

	/**
	 * Get latest timestamp of imu data that has been read from the capture class.
	 * @return Latest processed imu data timestamp; returns -1 if no data was processed or stream is unavailable.
	 */
	[[nodiscard]] int64_t getImuSeekTime() const {
		return mBuffers.getImuSeekTime();
	}

	/**
	 * Get latest timestamp of trigger data stream that has been read from the capture class.
	 * @return Latest processed trigger timestamp; returns -1 if no data was processed or stream is unavailable.
	 */
	[[nodiscard]] int64_t getTriggerSeekTime() const {
		return mBuffers.getTriggerSeekTime();
	}

	/**
	 * Read a packet from the camera and return a variant of any packet. You can use std::visit
	 * with `dv::io::DataReadHandler` to handle each type of packet using callback methods. This method
	 * might not maintain timestamp monotonicity between different stream types.
	 *
	 * @return			A variant containing data packet from the camera.
	 */
	[[nodiscard]] DataReadVariant readNext() {
		if (!isRunning()) {
			return DataReadHandler::OutputFlag::END_OF_FILE;
		}

		switch (auto nextPacket = mBuffers.readNextPacket(); nextPacket.index()) {
			case 1: // Events.
				return dv::EventStore{std::make_shared<dv::EventPacket>(std::get<1>(std::move(nextPacket)))};

			case 2: // Frames.
				return std::get<2>(std::move(nextPacket));

			case 3: // IMU.
				return std::get<3>(std::move(nextPacket)).elements;

			case 4: // Triggers.
				return std::get<4>(std::move(nextPacket)).elements;

			default: // Ignore.
				return DataReadHandler::OutputFlag::CONTINUE;
		}
	}

	/**
	 * Read next packet from the camera and use a handler object to handle all types of packets. The function returns
	 * a true if end-of-file was not reached, so this function call can be used in a while loop like so:
	 * ```
	 * while (camera.handleNext(handler)) {
	 * 		// While-loop executes after each packet
	 * }
	 * ```
	 * @param handler	Handler instance that contains callback functions to handle different packets.
	 * @return			False to indicate end of data stream, true to continue.
	 */
	[[nodiscard]] bool handleNext(DataReadHandler &handler) {
		std::visit(handler, readNext());
		return !handler.eof;
	}

protected:
	template<typename T, size_t BUFFER_SIZE = 256>
	struct SortedPackets {
		int64_t mSeekTime = -1;
		boost::circular_buffer<std::pair<uint64_t, T>> mData{BUFFER_SIZE};
	};

	class SortedPacketBuffers {
	public:
		mutable std::mutex mDataLock{};

		uint64_t mPacketCount{0};

		SortedPackets<dv::EventPacket> mEvents{};
		SortedPackets<dv::Frame> mFrames{};
		SortedPackets<dv::IMUPacket> mImu{};
		SortedPackets<dv::TriggerPacket> mTriggers{};

		void acceptData(parser::ParsedData data) {
			std::scoped_lock lock{mDataLock};

			if (!data.events.elements.empty()) {
				mEvents.mData.push_back(std::make_pair(mPacketCount++, std::move(data.events)));
			}

			if (!data.frames.empty()) {
				for (auto &frame : data.frames) {
					mFrames.mData.push_back(std::make_pair(mPacketCount++, std::move(frame)));
				}
			}

			if (!data.imu.elements.empty()) {
				mImu.mData.push_back(std::make_pair(mPacketCount++, std::move(data.imu)));
			}

			if (!data.triggers.elements.empty()) {
				mTriggers.mData.push_back(std::make_pair(mPacketCount++, std::move(data.triggers)));
			}
		}

		void clearBuffers() {
			std::scoped_lock lock{mDataLock};

			mEvents.mData.clear();
			mFrames.mData.clear();
			mImu.mData.clear();
			mTriggers.mData.clear();

			mEvents.mSeekTime   = -1;
			mFrames.mSeekTime   = -1;
			mImu.mSeekTime      = -1;
			mTriggers.mSeekTime = -1;
		}

		[[nodiscard]] std::optional<dv::EventPacket> popEventPacket() {
			std::scoped_lock lock{mDataLock};

			if (mEvents.mData.empty()) {
				return std::nullopt;
			}

			std::pair<size_t, dv::EventPacket> events = std::move(mEvents.mData.front());
			mEvents.mData.pop_front();

			mEvents.mSeekTime = events.second.elements.back().timestamp();
			return events.second;
		}

		[[nodiscard]] std::optional<dv::Frame> popFrame() {
			std::scoped_lock lock{mDataLock};

			if (mFrames.mData.empty()) {
				return std::nullopt;
			}

			std::pair<size_t, dv::Frame> frame = std::move(mFrames.mData.front());
			mFrames.mData.pop_front();

			mFrames.mSeekTime = frame.second.timestamp;
			return frame.second;
		}

		[[nodiscard]] std::optional<dv::IMUPacket> popImuPacket() {
			std::scoped_lock lock{mDataLock};

			if (mImu.mData.empty()) {
				return std::nullopt;
			}

			std::pair<size_t, dv::IMUPacket> imu = std::move(mImu.mData.front());
			mImu.mData.pop_front();

			mImu.mSeekTime = imu.second.elements.back().timestamp;
			return imu.second;
		}

		[[nodiscard]] std::optional<dv::TriggerPacket> popTriggerPacket() {
			std::scoped_lock lock{mDataLock};

			if (mTriggers.mData.empty()) {
				return std::nullopt;
			}

			std::pair<size_t, dv::TriggerPacket> triggers = std::move(mTriggers.mData.front());
			mTriggers.mData.pop_front();

			mTriggers.mSeekTime = triggers.second.elements.back().timestamp;
			return triggers.second;
		}

		/**
		 * Get latest timestamp of event data stream that has been read from the capture class.
		 * @return Latest processed event timestamp; returns -1 if no data was processed or stream is unavailable.
		 */
		[[nodiscard]] int64_t getEventSeekTime() const {
			std::scoped_lock lock{mDataLock};
			return mEvents.mSeekTime;
		}

		/**
		 * Get latest timestamp of frames stream that has been read from the capture class.
		 * @return Latest processed frame timestamp; returns -1 if no data was processed or stream is unavailable.
		 */
		[[nodiscard]] int64_t getFrameSeekTime() const {
			std::scoped_lock lock{mDataLock};
			return mFrames.mSeekTime;
		}

		/**
		 * Get latest timestamp of imu data that has been read from the capture class.
		 * @return Latest processed imu data timestamp; returns -1 if no data was processed or stream is unavailable.
		 */
		[[nodiscard]] int64_t getImuSeekTime() const {
			std::scoped_lock lock{mDataLock};
			return mImu.mSeekTime;
		}

		/**
		 * Get latest timestamp of trigger data stream that has been read from the capture class.
		 * @return Latest processed trigger timestamp; returns -1 if no data was processed or stream is unavailable.
		 */
		[[nodiscard]] int64_t getTriggerSeekTime() const {
			std::scoped_lock lock{mDataLock};
			return mTriggers.mSeekTime;
		}

		[[nodiscard]] std::variant<std::monostate, dv::EventPacket, dv::Frame, dv::IMUPacket, dv::TriggerPacket>
			readNextPacket() {
			std::array<uint64_t, 4> indices{};
			indices.fill(std::numeric_limits<uint64_t>::max());

			std::scoped_lock lock{mDataLock};

			// Find the packet with lowest packet index
			if (!mEvents.mData.empty()) {
				indices[0] = mEvents.mData.front().first;
			}
			if (!mFrames.mData.empty()) {
				indices[1] = mFrames.mData.front().first;
			}
			if (!mImu.mData.empty()) {
				indices[2] = mImu.mData.front().first;
			}
			if (!mTriggers.mData.empty()) {
				indices[3] = mTriggers.mData.front().first;
			}

			const auto min = std::min_element(indices.begin(), indices.end());
			if ((min == indices.end()) || (*min == std::numeric_limits<uint64_t>::max())) {
				return {}; // std::monostate
			}

			// Read the specified packet from the according packet buffer
			switch (std::distance(indices.begin(), min)) {
				case 0: {
					std::pair<size_t, dv::EventPacket> events = std::move(mEvents.mData.front());
					mEvents.mData.pop_front();

					mEvents.mSeekTime = events.second.elements.back().timestamp();
					return events.second;
				}

				case 1: {
					std::pair<size_t, dv::Frame> frame = std::move(mFrames.mData.front());
					mFrames.mData.pop_front();

					mFrames.mSeekTime = frame.second.timestamp;
					return frame.second;
				}

				case 2: {
					std::pair<size_t, dv::IMUPacket> imu = std::move(mImu.mData.front());
					mImu.mData.pop_front();

					mImu.mSeekTime = imu.second.elements.back().timestamp;
					return imu.second;
				}

				case 3: {
					std::pair<size_t, dv::TriggerPacket> triggers = std::move(mTriggers.mData.front());
					mTriggers.mData.pop_front();

					mTriggers.mSeekTime = triggers.second.elements.back().timestamp;
					return triggers.second;
				}

				default:
					return {}; // std::monostate
			}
		}
	};

	SortedPacketBuffers mBuffers;
};

using CameraPtr = std::unique_ptr<CameraInputBase>;

} // namespace dv::io::camera

// fmt compatibility for enum class printing.
template<>
struct fmt::formatter<dv::io::camera::CameraInputBase::Flatten> : fmt::ostream_formatter {};
