#pragma once

#include "parsers/dvs128_parser.hpp"
#include "sync_camera_input_base.hpp"
#include "usb_device.hpp"

namespace dv::io::camera {

class DVS128 final : public USBDevice, public SyncCameraInputBase {
public:
	enum class Bias {
		CAS      = 0,
		INJGND   = 1,
		REQPD    = 2,
		PUX      = 3,
		DIFF_OFF = 4,
		REQ      = 5,
		REFR     = 6,
		PUY      = 7,
		DIFF_ON  = 8,
		DIFF     = 9,
		FOLL     = 10,
		PR       = 11,
	};

	/**
	 * Find connected DVS128 cameras.
	 * @param filterBySerialNumber only search for devices with this serial number
	 * @param logger discovery logging callback, must be thread-safe
	 * @return a descriptor structure describing a compatible device
	 */
	[[nodiscard]] static auto findDevices(
		const std::string_view filterBySerialNumber = {}, loggerCallbackType logger = {}) {
		const auto found = findCompatibleDevices(COMPATIBLE_CAMERA, std::move(logger));

		return filterCompatibleDevices(found, filterBySerialNumber);
	}

	/**
	 * Open the first DVS128 camera that can be found.
	 * Throws if device cannot be opened.
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVS128(loggerCallbackType deviceLogger = {}, const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVS128(findDevices().at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVS128 camera with the specified serial number.
	 * Throws if device cannot be opened.
	 * @param filterBySerialNumber serial number to search for
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVS128(const std::string_view filterBySerialNumber, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DVS128(findDevices(filterBySerialNumber).at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DVS128 camera corresponding to the specified descriptor.
	 * Throws if device cannot be opened.
	 * @param deviceToOpen device descriptor structure
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DVS128(const DeviceDescriptor &deviceToOpen, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		USBDevice(deviceToOpen, std::move(deviceLogger), deviceLogLevel, "dvs128", "DVS128", COMPATIBLE_CAMERA,
			FIRMWARE_REQUIRED_VERSION) {
		// USB and parser setup.
		setDataCallback(std::bind_front(&DVS128::usbDataCallback, this));
		setShutdownCallback(std::bind_front(&DVS128::shutdownCallback, this));

		mParser.setLogger([this](const bool debug, const std::string_view message) {
			log((debug) ? (LogLevel::LVL_DEBUG) : (LogLevel::LVL_ERROR), message);
		});

		mParser.setDataCommitCallback(std::bind_front(&DVS128::dataParserCallback, this));
		mParser.setTimeInitCallback(std::bind_front(&DVS128::timeInitCallback, this));

		setDataEndpoint(DATA_ENDPOINT);

		// Device default configuration.
		setMaster(true);

		// Based on DVS128 Fast biases.
		setBiasInternal(Bias::CAS, 1992, false);
		setBiasInternal(Bias::INJGND, 1108364, false);
		setBiasInternal(Bias::REQPD, 16777215, false);
		setBiasInternal(Bias::PUX, 8159221, false);
		setBiasInternal(Bias::DIFF_OFF, 132, false);
		setBiasInternal(Bias::REQ, 309590, false);
		setBiasInternal(Bias::REFR, 969, false);
		setBiasInternal(Bias::PUY, 16777215, false);
		setBiasInternal(Bias::DIFF_ON, 209996, false);
		setBiasInternal(Bias::DIFF, 13125, false);
		setBiasInternal(Bias::FOLL, 271, false);
		setBiasInternal(Bias::PR, 217, true);

		setFlatten(Flatten::NONE);

		setCropArea({
			{0, 0},
            *getEventResolution()
        });

		usbDataTransfersStart();

		try {
			// Enable data producers.
			usbControlTransferOut(VENDOR_REQUEST_START_TRANSFER, 0, 0, {});

			// Timestamp reset to sync up time.
			sendTimestampReset();
			waitForTimestampReset();
		}
		catch (const dv::exceptions::RuntimeError &) {
			usbDataTransfersStop();

			throw;
		}
	}

	~DVS128() override {
		try {
			usbControlTransferOut(VENDOR_REQUEST_STOP_TRANSFER, 0, 0, {});
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Ignore error, usually because device is gone. Always try to shut-down, as the device might not have gone
			// away fully and can still receive the command.
		}

		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		usbDataTransfersStop();
	}

	[[nodiscard]] std::string getCameraName() const override {
		return getDeviceName();
	}

	[[nodiscard]] std::optional<cv::Size> getEventResolution() const override {
		return cv::Size{parser::DVS128::WIDTH, parser::DVS128::HEIGHT};
	}

	[[nodiscard]] std::optional<cv::Size> getFrameResolution() const override {
		return std::nullopt;
	}

	[[nodiscard]] imu::ImuModel getImuModel() const override {
		return imu::ImuModel::IMU_NONE;
	}

	[[nodiscard]] float getPixelPitch() const override {
		return 40e-6f;
	}

	[[nodiscard]] bool isMaster() const override {
		return mTimestampMaster;
	}

	/**
	 * 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.
	 * @param master true if clock synchronization master, false if secondary camera
	 */
	void setMaster(const bool master) {
		usbControlTransferOut(VENDOR_REQUEST_TS_MASTER, master, 0, {});
		mTimestampMaster = master;
	}

	[[nodiscard]] bool isEventStreamAvailable() const override {
		// All supported cameras have event output right now
		return true;
	}

	[[nodiscard]] bool isFrameStreamAvailable() const override {
		return false;
	}

	[[nodiscard]] bool isImuStreamAvailable() const override {
		return false;
	}

	[[nodiscard]] bool isTriggerStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isRunning() const override {
		return mIsRunning;
	}

	[[nodiscard]] bool isRunning(const std::string_view streamName) const override {
		if (streamName == "events") {
			return isRunning();
		}
		if (streamName == "triggers") {
			return isRunning() && isMaster();
		}

		throw dv::exceptions::InvalidArgument<std::string>(
			"Stream with a given name is not available", std::string{streamName});
	}

	[[nodiscard]] bool isRunningAny() const override {
		return isRunning("events") || isRunning("triggers");
	}

	[[nodiscard]] std::chrono::microseconds getTimeInterval() const override {
		return mParser.getTimeInterval();
	}

	void setTimeInterval(const std::chrono::microseconds timeInterval) override {
		mParser.setTimeInterval(timeInterval);
	}

	[[nodiscard]] std::chrono::microseconds getTimestampOffset() const override {
		return mParser.getSystemOffset();
	}

	/**
	 * Get current value of specified bias configuration.
	 * @param bias name of bias
	 * @return current bias value
	 */
	[[nodiscard]] uint32_t getBias(const Bias bias) const {
		std::scoped_lock lock{mConfigLock};

		uint32_t value{0};

		value |= static_cast<uint32_t>(mBiases[dv::EnumAsInteger(bias)][0] << 16);
		value |= static_cast<uint32_t>(mBiases[dv::EnumAsInteger(bias)][1] << 8);
		value |= static_cast<uint32_t>(mBiases[dv::EnumAsInteger(bias)][2] << 0);

		return value;
	}

	/**
	 * Set new value for specified bias configuration.
	 * @param bias name of bias
	 * @param value new bias value
	 */
	void setBias(const Bias bias, const uint32_t value) {
		std::scoped_lock lock{mConfigLock};

		setBiasInternal(bias, value, true);
	}

	[[nodiscard]] bool getFlipHorizontal() const override {
		return mParser.getDVSFlipHorizontal();
	}

	void setFlipHorizontal(const bool flipHorizontal) override {
		mParser.setDVSFlipHorizontal(flipHorizontal);
	}

	[[nodiscard]] bool getFlipVertical() const override {
		return mParser.getDVSFlipVertical();
	}

	void setFlipVertical(const bool flipVertical) override {
		mParser.setDVSFlipVertical(flipVertical);
	}

	[[nodiscard]] Flatten getFlatten() const override {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		return mFlatten;
	}

	void setFlatten(const Flatten flatten) override {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		mFlatten = flatten;

		mParser.setDVSConvertAllOn(mFlatten == Flatten::FLATTEN_ON);
	}

	[[nodiscard]] cv::Rect getCropArea() const override {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		return mCropArea;
	}

	void setCropArea(const cv::Rect cropArea) override {
		if ((cropArea.x >= getEventResolution()->width)
			|| ((cropArea.x + cropArea.width - 1) >= getEventResolution()->width)
			|| (cropArea.y >= getEventResolution()->height)
			|| ((cropArea.y + cropArea.height - 1) >= getEventResolution()->height)) {
			throw dv::exceptions::InvalidArgument<cv::Rect>("The crop area is invalid.");
		}

		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		mCropArea = cropArea;
	}

protected:
	void sendTimestampReset() override {
		mSyncState = SyncState::WAIT_RESET;

		usbControlTransferOut(VENDOR_REQUEST_RESET_TS, 0, 0, {});
	}

	void setTimestampOffset(const std::chrono::microseconds timestampOffset) override {
		mParser.setSystemOffset(timestampOffset);
		mBuffers.clearBuffers();
	}

private:
	static constexpr uint16_t PID_DVS128{0x8400};
	static constexpr uint8_t FIRMWARE_REQUIRED_VERSION{14};

	static constexpr uint8_t DATA_ENDPOINT{0x86};

	static constexpr uint8_t VENDOR_REQUEST_START_TRANSFER{0xB3};
	static constexpr uint8_t VENDOR_REQUEST_STOP_TRANSFER{0xB4};
	static constexpr uint8_t VENDOR_REQUEST_SEND_BIASES{0xB8};
	static constexpr uint8_t VENDOR_REQUEST_RESET_TS{0xBB};
	static constexpr uint8_t VENDOR_REQUEST_RESET_ARRAY{0xBD};
	static constexpr uint8_t VENDOR_REQUEST_TS_MASTER{0xBE};

	static constexpr auto COMPATIBLE_CAMERA
		= [](const uint16_t vid, const uint16_t pid,
			  [[maybe_unused]] const USBDeviceType deviceType) -> std::optional<CameraModel> {
		if ((vid == VID_INIVATION) && (pid == PID_DVS128)) {
			return CameraModel::DVS128;
		}

		return std::nullopt;
	};

	// Set during construction.
	parser::DVS128::Parser mParser;
	// Multi-part configuration needing locking.
	mutable std::mutex mConfigLock;
	std::array<std::array<uint8_t, 3>, 12> mBiases{};
	Flatten mFlatten{Flatten::NONE};
	cv::Rect mCropArea;
	// Separate lock for USB callback.
	mutable std::mutex mCallbackConfigLock;
	// Atomic due to USB callback use.
	std::atomic<bool> mIsRunning{true};
	std::atomic<bool> mTimestampMaster{true};

	void shutdownCallback() {
		mIsRunning = false;
	}

	void usbDataCallback(const std::span<const uint8_t> data) {
		// Return right away if not running anymore. This prevents useless work.
		if (!mIsRunning || !isUSBDataTransferRunning()) {
			return;
		}

		try {
			mParser.parseData(data, false);
		}
		catch (const dv::exceptions::RuntimeError &ex) {
			// Critical parsing failure (eg. timestamp error). Disable camera by setting mIsRunning=false.
			// Since we are inside the USB callback, we cannot stop the USB transfers from here. They'll continue
			// running but not be actually parsed since we are in an unrecoverable state only fit for destruction.
			mIsRunning = false;
			log(LogLevel::LVL_ERROR, ex.what());
		}
	}

	void dataParserCallback(parser::ParsedData data) {
		if (!data.events.elements.empty()) {
			std::scoped_lock lock{mCallbackConfigLock};

			std::vector<dv::Event> filtered{};
			const bool filterPolarity = (mFlatten == Flatten::ON_ONLY) || (mFlatten == Flatten::OFF_ONLY);
			const bool polarityToDrop = (mFlatten == Flatten::OFF_ONLY);

			// Apply software filters.
			for (const auto &event : data.events.elements) {
				if (filterPolarity && (event.polarity() == polarityToDrop)) {
					continue;
				}

				if (!mCropArea.contains(cv::Point{event.x(), event.y()})) {
					continue;
				}

				filtered.push_back(event);
			}

			data.events.elements = std::move(filtered);
		}

		mBuffers.acceptData(std::move(data));
	}

	void timeInitCallback() {
		mSyncState = SyncState::GOT_RESET;

		mBuffers.clearBuffers();
	}

	void setBiasInternal(const Bias bias, const uint32_t value, const bool send) {
		if (value >= (0x01 << 24)) {
			throw dv::exceptions::InvalidArgument<uint32_t>("The bias value is invalid.");
		}

		mBiases[dv::EnumAsInteger(bias)][0] = static_cast<uint8_t>(value >> 16);
		mBiases[dv::EnumAsInteger(bias)][1] = static_cast<uint8_t>(value >> 8);
		mBiases[dv::EnumAsInteger(bias)][2] = static_cast<uint8_t>(value >> 0);

		if (send) {
			// Biases are already stored in an array with the same format as expected by
			// the device, we can thus send it directly.
			usbControlTransferOut(VENDOR_REQUEST_SEND_BIASES, 0, 0, {mBiases[0].data(), sizeof(mBiases)});
		}
	}
};

} // namespace dv::io::camera
