#pragma once

#include "../core/frame.hpp"
#include "../exception/exceptions/generic_exceptions.hpp"

namespace dv {

/**
 * A base class for noise filter implementations. Handles data input and output, derived classes only have to
 * implement a retain function that tests whether event should be retained or discarded.
 */
template<dv::concepts::EventStorage EventStoreClass = dv::EventStore>
class EventFilterBase {
protected:
	EventStoreClass mBuffer{};
	int64_t mHighestProcessedTime{-1};
	size_t mNumberIncomingEvents{0};
	size_t mNumberOutgoingEvents{0};

public:
	virtual ~EventFilterBase() = default;

	/**
	 * Accepts incoming events.
	 * @param store 	Event packet.
	 */
	void accept(const EventStoreClass &store) {
		if (store.isEmpty()) {
			return;
		}

		if (store.getLowestTime() < mHighestProcessedTime) {
			throw std::out_of_range{"Tried adding event store to store out of order. Ignoring packet."};
		}

		mBuffer.add(store);
	}

	/**
	 * A function to be implemented by derived class which tests whether given event
	 * should be retained or discarded.
	 * @param event 	An event to be checked.
	 * @return 			Return true if the event is to be retained or false to discard the event.
	 */
	[[nodiscard]] virtual bool retain(const typename EventStoreClass::value_type &event) noexcept = 0;

	/**
	 * Apply the filter algorithm and return only the filtered events from the ones that were accepted as input.
	 * @return
	 */
	[[nodiscard]] EventStoreClass generateEvents() {
		if (!mBuffer.isEmpty()) {
			mNumberIncomingEvents += mBuffer.size();

			std::shared_ptr<typename EventStoreClass::packet_type> packet
				= std::make_shared<typename EventStoreClass::packet_type>();
			packet->elements.reserve(mBuffer.size());

			for (const auto &event : mBuffer) {
				if (retain(event)) {
					packet->elements.push_back(event);
				}
			}

			packet->elements.shrink_to_fit();
			mNumberOutgoingEvents += packet->elements.size();
			mHighestProcessedTime  = mBuffer.getHighestTime();

			mBuffer = EventStoreClass{};
			return EventStoreClass(std::const_pointer_cast<typename EventStoreClass::const_packet_type>(packet));
		}

		return {};
	}

	/**
	 * Get number of total events that were accepted by the noise filter.
	 * @return 		Total number of incoming events to this filter instance.
	 */
	[[nodiscard]] size_t getNumberOfIncomingEvents() const {
		return mNumberIncomingEvents;
	}

	/**
	 * Total number of outgoing events from this filter instance.
	 * @return 		Total number of outgoing events from this filter instance.
	 */
	[[nodiscard]] size_t getNumberOfOutgoingEvents() const {
		return mNumberOutgoingEvents;
	}

	/**
	 * Get the reduction factor of this filter. It's a fraction representation of events that were discard by this
	 * filter compared to the amount of incoming events.
	 * @return 		Reduction factor value.
	 */
	[[nodiscard]] float getReductionFactor() const {
		if (mNumberIncomingEvents > 0) {
			return 1.f - (static_cast<float>(mNumberOutgoingEvents) / static_cast<float>(mNumberIncomingEvents));
		}

		return 0.f;
	}

	/**
	 * Retrieve filtered events using output stream operator.
	 * @param out 		Filtered events.
	 * @return
	 */
	EventStoreClass &operator>>(EventStoreClass &out) {
		out = generateEvents();
		return out;
	}
};

/**
 * Event filter based on multiple event filter applied sequentially. Internally stores
 * any added filters and runs them one after another.
 * @tparam EventStoreClass 	Type of event store
 */
template<dv::concepts::EventStorage EventStoreClass = dv::EventStore>
class EventFilterChain : public EventFilterBase<EventStoreClass> {
protected:
	std::vector<std::shared_ptr<dv::EventFilterBase<EventStoreClass>>> filters;

public:
	/**
	 * Add a filter to the chain of filtering.
	 * @param filter
	 */
	void addFilter(std::shared_ptr<dv::EventFilterBase<EventStoreClass>> filter) {
		filters.push_back(std::move(filter));
	}

	/**
	 * Test whether event is of configured polarity.
	 * @param event 	Event to be checked.
	 * @return 			True if event has the expected polarity, false otherwise.
	 */
	[[nodiscard]] bool retain(const typename EventStoreClass::value_type &event) noexcept override {
		for (const auto &filter : filters) {
			if (!filter->retain(event)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Accept events using the input stream operator.
	 * @param events 	Input events.
	 * @return
	 */
	EventFilterChain &operator<<(const EventStoreClass &events) {
		accept(events);
		return *this;
	}
};

static_assert(dv::concepts::EventFilter<EventFilterChain<>, dv::EventStore>);

/**
 * Event filter that filters events based on a given ROI.
 * @tparam EventStoreClass 	Type of event store
 */
template<dv::concepts::EventStorage EventStoreClass = dv::EventStore>
class EventRegionFilter : public EventFilterBase<EventStoreClass> {
protected:
	cv::Rect mRegion;

public:
	/**
	 * Filter event based on an ROI.
	 * @param region		Region of interest, events outside of this region will be discarded.
	 */
	explicit EventRegionFilter(const cv::Rect region) : mRegion(region) {
	}

	/**
	 * Get the currently applied region of interest.
	 * @return
	 */
	[[nodiscard]] cv::Rect getRegion() const {
		return mRegion;
	}

	/**
	 * Set a new region of interest for events.
	 * @param region		Region of interest, events outside of this region will be discarded.
	 */
	void setRegion(const cv::Rect region) {
		mRegion = region;
	}

	/**
	 * Test whether event belongs to an ROI.
	 * @param event 	Event to be checked.
	 * @return 			True if event belongs to ROI, false otherwise.
	 */
	[[nodiscard]] bool retain(const typename EventStoreClass::value_type &event) noexcept override {
		return mRegion.contains(cv::Point2i(event.x(), event.y()));
	}

	/**
	 * Accept events using the input stream operator.
	 * @param events 	Input events.
	 * @return
	 */
	EventRegionFilter &operator<<(const EventStoreClass &events) {
		accept(events);
		return *this;
	}
};

static_assert(concepts::EventFilter<EventRegionFilter<>, dv::EventStore>);

/**
 * Event filter based on polarity.
 * @tparam EventStoreClass 	Type of event store
 */
template<dv::concepts::EventStorage EventStoreClass = dv::EventStore>
class EventPolarityFilter : public EventFilterBase<EventStoreClass> {
protected:
	bool mPolarity;

public:
	/**
	 * Construct an event filter which filters out only events of given polarity.
	 * @param polarity 		Extract events only of matching polarity.
	 */
	explicit EventPolarityFilter(const bool polarity) : mPolarity(polarity) {
	}

	/**
	 * Get the currently extracted polarity setting.
	 * @return
	 */
	[[nodiscard]] bool getPolarity() const {
		return mPolarity;
	}

	/**
	 * Set a new polarity to extract (keep after filtering).
	 * @param polarity 		Extract events only of matching polarity.
	 */
	void setPolarity(const bool polarity) {
		mPolarity = polarity;
	}

	/**
	 * Test whether event is of configured polarity.
	 * @param event 	Event to be checked.
	 * @return 			True if event has the expected polarity, false otherwise.
	 */
	[[nodiscard]] bool retain(const typename EventStoreClass::value_type &event) noexcept override {
		return (event.polarity() == mPolarity);
	}

	/**
	 * Accept events using the input stream operator.
	 * @param events 	Input events.
	 * @return
	 */
	EventPolarityFilter &operator<<(const EventStoreClass &events) {
		accept(events);
		return *this;
	}
};

static_assert(concepts::EventFilter<EventPolarityFilter<>, dv::EventStore>);

template<dv::concepts::EventStorage EventStoreClass = dv::EventStore>
class EventMaskFilter : public EventFilterBase<EventStoreClass> {
protected:
	cv::Mat mMask;

public:
	/**
	 * Create an event masking filter. Discards any events that happen on coordinates where mask has a zero
	 * value and retains all events with coordinates where mask has a non-zero value.
	 * @param mask 		The mask to be applied (requires CV_8UC1 type).
	 * @throws InvalidArgument	Exception thrown if the mask is of incorrect type.
	 */
	explicit EventMaskFilter(const cv::Mat &mask) : mMask(mask) {
		if (mask.type() != CV_8UC1) {
			throw dv::exceptions::InvalidArgument<int>(
				"Invalid mask type, expecting a mask of a single channel 8-bit unsigned"
				"integer matrix (CV_8UC1).",
				mask.type());
		}
	}

	bool retain(const typename EventStoreClass::value_type &event) noexcept override {
		return (mMask.at<uint8_t>(event.y(), event.x()) > 0);
	}

	/**
	 * Get the mask that is currently applied.
	 * @return
	 */
	[[nodiscard]] const cv::Mat &getMask() const {
		return mMask;
	}

	/**
	 * Set a new mask to this filter.
	 * @param mask 		The mask to be applied (requires CV_8UC1 type).
	 */
	void setMask(const cv::Mat &mask) {
		mMask = mask;
	}

	/**
	 * Accept events using the input stream operator.
	 * @param events 	Input events.
	 * @return
	 */
	EventMaskFilter &operator<<(const EventStoreClass &events) {
		accept(events);
		return *this;
	}
};

static_assert(dv::concepts::EventFilter<EventMaskFilter<>, dv::EventStore>);

} // namespace dv
