#pragma once

#include "../../../include/dv-processing/core/core.hpp"

#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

namespace py = pybind11;

/**
 * @brief Bind EventStore class to python
 * @param m pybind The module to bind to
 */
void bind_event_store(py::module &m) {
	using pybind11::operator""_a;

	py::class_<dv::EventStore>(m, "EventStore")
		.def(py::init<>())
		.def(
			"push_back",
			[](dv::EventStore &self, const py::tuple &t) {
				if (py::len(t) != 4) {
					throw std::runtime_error("dv::EventStore requires a four element tuple representing an event in a "
											 "format (timestamp, x, y, polarity)");
				}
				self.emplace_back(t[0].cast<int64_t>(), t[1].cast<int16_t>(), t[2].cast<int16_t>(), t[3].cast<bool>());
			},
			py::arg("event"))
		.def("size", &dv::EventStore::size, "Get the numbers of stored events.")
		.def("getHighestTime", &dv::EventStore::getHighestTime, "Highest timestamp stored.")
		.def("getLowestTime", &dv::EventStore::getLowestTime, "Lowest timestamp stored.")
		.def("at", &dv::EventStore::at, py::arg("index"), "Get an event at given index.")
		.def("isEmpty", &dv::EventStore::isEmpty, "True if the event store is empty, False otherwise.")
		.def("rate", &dv::EventStore::rate,
			"Get the event rate (events per second) for the events stored in this storage.")
		.def("retainDuration", &dv::EventStore::retainDuration, "duration"_a,
			"Retain a given duration of events, calculating the duration from the latest available event.")
		.def("sliceBack", &dv::EventStore::sliceBack, "length"_a,
			"Returns a slice which contains events from the back of the storage, it will contain no more events than "
			"given length variable.")
		.def("sliceRate", &dv::EventStore::sliceRate, "targetRate"_a,
			"Slices events from back of the EventStore, so that the EventStore would only contain given events of a "
			"given target rate.")
		.def(
			"slice",
			[](const dv::EventStore &self, const size_t start) {
				return self.slice(start);
			},
			py::arg("start"), "Forward slice of events from a given index.")
		.def(
			"slice",
			[](const dv::EventStore &self, const size_t start, const size_t length) {
				return self.slice(start, length);
			},
			py::arg("start"), py::arg("length"),
			"Returns a new EventStore which is a shallow representation of a slice of this EventStore. The slice is "
			"from `start` (number of events, minimum 0, maximum `size()`) and has a length of `length`.")
		.def(
			"add",
			[](dv::EventStore &self, const dv::EventStore &other) {
				return self.add(other);
			},
			py::arg("other"), "Adds all the events of the other event store to this event store.")
		.def(
			"push_back",
			[](dv::EventStore &self, const int64_t timestamp, const int16_t x, const int16_t y, const bool polarity) {
				return self.emplace_back(timestamp, x, y, polarity);
			},
			py::arg("timestamp"), py::arg("x"), py::arg("y"), py::arg("polarity"),
			"Adds the given event to the end of this EventStore.")
		.def(
			"push_back",
			[](dv::EventStore &self, const dv::Event &event) {
				return self.push_back(event);
			},
			"event"_a, "Adds the given event to the end of this EventStore.")
		.def("erase", &dv::EventStore::erase, py::arg("start"), py::arg("length"),
			"Erase given range of events from the event store. This does not necessarily delete the underlying data "
			"since event store maps the data using smart pointers, the data will be cleared only in the case "
			"that none of the stores is mapping the data. This erase function does not affect data shared with other "
			"event stores.")
		.def("eraseTime", &dv::EventStore::eraseTime, py::arg("startTime"), py::arg("endTime"),
			"Erase events in the range between given timestamps. This does not necessarily delete the underlying data "
			"since event store maps the data using smart pointers, the data will be cleared only in the case "
			"that none of the stores is mapping the data. This erase function does not affect data shared with other "
			"event stores.")
		.def("front", &dv::EventStore::front, "Returns the first element of the store")
		.def("back", &dv::EventStore::back, "Returns the last element of the store")
		.def("duration", &dv::EventStore::duration,
			"Return a timedelta representing the duration of events contained in this store.")
		.def("isWithinStoreTimeRange", &dv::EventStore::isWithinStoreTimeRange, py::arg("timestamp"),
			"Checks whether given timestamp is within the time range of the event store.")
		.def(
			"sliceTime",
			[](const dv::EventStore &self, const int64_t startTime, const int64_t endTime) -> dv::EventStore {
				return self.sliceTime(startTime, endTime);
			},
			py::arg("startTime"), py::arg("endTime"),
			"Returns a new EventStore which is a shallow representation of a slice of this EventStore. The slice is "
			"from a specific startTime (in event timestamps, microseconds) to a specific endTime (event timestamps, "
			"microseconds). The actual size (in events) of the resulting packet depends on the event rate in the "
			"requested time interval. The resulting packet may be empty, if there is no event that happend in the "
			"requested interval.")
		.def(
			"sliceTime",
			[](const dv::EventStore &self, const int64_t startTime) -> dv::EventStore {
				return self.sliceTime(startTime);
			},
			py::arg("startTime"),
			"Returns a new EventStore which is a shallow representation of a slice of this EventStore. The slice is "
			"from a specific startTime (in event timestamps, microseconds) to the end of the packet. The actual size "
			"(in events) of the resulting packet depends on the event rate in the requested time interval. The "
			"resulting packet may be empty, if there is no event that happened in the requested interval.")
		.def("timestamps", &dv::EventStore::timestamps,
			"Retrieve timestamps of events into a one-dimensional eigen matrix. This performs a copy of the values. "
			"The values are guaranteed to be monotonically increasing.")
		.def("coordinates", &dv::EventStore::coordinates,
			"Retrieve coordinates of events in a 2xN eigen matrix. Method performs a copy of the values. Coordinates "
			"maintain the same order as within the event store. First column is the x coordinate, second column is the "
			"y coordinate.")
		.def("polarities", &dv::EventStore::polarities,
			"Retrieve polarities of events in a one-dimensional eigen matrix. Method performs a copy of the values. "
			"Polarities maintain the same order as within the event store. Polarities are converted into unsigned "
			"8-bit integer values, where 0 stands for negative polarity event and 1 stands for positive polarity "
			"event.")
		.def(
			"numpy",
			[](const dv::EventStore &self) {
				// Convert into a continuous memory buffer
				auto *events = new dv::EventPacket(self.toPacket());

				// Deallocator
				py::capsule deallocate(events, [](void *ptr) {
					delete reinterpret_cast<dv::EventPacket *>(ptr);
				});

				py::list names;
				names.append("timestamp");
				names.append("x");
				names.append("y");
				names.append("polarity");
				py::list formats;
				formats.append("<i8");
				formats.append("<i2");
				formats.append("<i2");
				formats.append("<i1");
				py::list offsets;
				offsets.append(0);
				offsets.append(8);
				offsets.append(10);
				offsets.append(12);
				return py::array(py::dtype(names, formats, offsets, sizeof(dv::Event)), events->elements.size(),
					events->elements.data(), deallocate);
			},
			"Convert the event store into a array with named columns.")
		.def("toPacket", &dv::EventStore::toPacket,
			"Convert event store into a continuous memory packet. This performs a deep copy of underlying data.")
		.def("__len__", &dv::EventStore::size)
		.def("__getitem__", &dv::EventStore::at)
		.def("__repr__",
			[](const dv::EventStore &self) {
				std::stringstream ss;
				ss << self;
				return ss.str();
			})
		.def(
			"__iter__",
			[](const dv::EventStore &self) {
				return py::make_iterator(self.begin(), self.end());
			},
			py::keep_alive<0, 1>());
}

/**
 * @brief Bind dv::DepthEventStore class to python
 * @param m pybind The module to bind to
 */
void bind_depth_event_store(py::module &m) {
	using pybind11::operator""_a;

	py::class_<dv::DepthEventStore>(m, "DepthEventStore")
		.def(py::init<>())
		.def(
			"push_back",
			[](dv::DepthEventStore &self, const py::tuple &t) {
				if (py::len(t) != 5) {
					throw std::runtime_error(
						"dv::DepthEventStore requires a five element tuple representing an event in a "
						"format (timestamp, x, y, polarity, depth)");
				}
				self.emplace_back(t[0].cast<int64_t>(), t[1].cast<int16_t>(), t[2].cast<int16_t>(), t[3].cast<bool>(),
					t[4].cast<uint16_t>());
			},
			py::arg("event"))
		.def("size", &dv::DepthEventStore::size, "Get the numbers of stored events.")
		.def("getHighestTime", &dv::DepthEventStore::getHighestTime, "Highest timestamp stored.")
		.def("getLowestTime", &dv::DepthEventStore::getLowestTime, "Lowest timestamp stored.")
		.def("at", &dv::DepthEventStore::at, py::arg("index"), "Get an event at given index.")
		.def("isEmpty", &dv::DepthEventStore::isEmpty, "True if the event store is empty, False otherwise.")
		.def("rate", &dv::DepthEventStore::rate,
			"Get the event rate (events per second) for the events stored in this storage.")
		.def("retainDuration", &dv::DepthEventStore::retainDuration, "duration"_a,
			"Retain a given duration of events, calculating the duration from the latest available event.")
		.def("sliceBack", &dv::DepthEventStore::sliceBack)
		.def(
			"slice",
			[](const dv::DepthEventStore &self, const size_t start) {
				return self.slice(start);
			},
			py::arg("start"), "Forward slice of events from a given index.")
		.def(
			"slice",
			[](const dv::DepthEventStore &self, const size_t start, const size_t length) {
				return self.slice(start, length);
			},
			py::arg("start"), py::arg("length"),
			"Returns a new DepthEventStore which is a shallow representation of a slice of this DepthEventStore. The "
			"slice is "
			"from `start` (number of events, minimum 0, maximum `size()`) and has a length of `length`.")
		.def(
			"add",
			[](dv::DepthEventStore &self, const dv::DepthEventStore &other) {
				return self.add(other);
			},
			py::arg("other"), "Adds all the events of the other event store to this event store.")
		.def(
			"push_back",
			[](dv::DepthEventStore &self, const int64_t timestamp, const int16_t x, const int16_t y,
				const bool polarity, const uint16_t depth) {
				return self.emplace_back(timestamp, x, y, polarity, depth);
			},
			py::arg("timestamp"), py::arg("x"), py::arg("y"), py::arg("polarity"), py::arg("depth"),
			"Adds the given event to the end of this EventStore.")
		.def(
			"push_back",
			[](dv::DepthEventStore &self, const dv::DepthEvent &event) {
				return self.push_back(event);
			},
			"event"_a, "Adds the given event to the end of this EventStore.")
		.def("erase", &dv::DepthEventStore::erase, py::arg("start"), py::arg("length"),
			"Erase given range of events from the event store. This does not necessarily delete the underlying data "
			"since event store maps the data using smart pointers, the data will be cleared only in the case "
			"that none of the stores is mapping the data. This erase function does not affect data shared with other "
			"event stores.")
		.def("eraseTime", &dv::DepthEventStore::eraseTime, py::arg("startTime"), py::arg("endTime"),
			"Erase events in the range between given timestamps. This does not necessarily delete the underlying data "
			"since event store maps the data using smart pointers, the data will be cleared only in the case "
			"that none of the stores is mapping the data. This erase function does not affect data shared with other "
			"event stores.")
		.def("front", &dv::DepthEventStore::front, "Returns the first element of the store")
		.def("back", &dv::DepthEventStore::back, "Returns the last element of the store")
		.def("duration", &dv::DepthEventStore::duration,
			"Return a timedelta representing the duration of events contained in this store.")
		.def("isWithinStoreTimeRange", &dv::DepthEventStore::isWithinStoreTimeRange, py::arg("timestamp"),
			"Checks whether given timestamp is within the time range of the event store.")
		.def(
			"sliceTime",
			[](const dv::DepthEventStore &self, const int64_t startTime, const int64_t endTime) -> dv::DepthEventStore {
				return self.sliceTime(startTime, endTime);
			},
			py::arg("startTime"), py::arg("endTime"),
			"Returns a new DepthEventStore which is a shallow representation of a slice of this DepthEventStore. The "
			"slice is "
			"from a specific startTime (in event timestamps, microseconds) to a specific endTime (event timestamps, "
			"microseconds). The actual size (in events) of the resulting packet depends on the event rate in the "
			"requested time interval. The resulting packet may be empty, if there is no event that happend in the "
			"requested interval.")
		.def(
			"sliceTime",
			[](const dv::DepthEventStore &self, const int64_t startTime) -> dv::DepthEventStore {
				return self.sliceTime(startTime);
			},
			py::arg("startTime"),
			"Returns a new DepthEventStore which is a shallow representation of a slice of this DepthEventStore. The "
			"slice is "
			"from a specific startTime (in event timestamps, microseconds) to the end of the packet. The actual size "
			"(in events) of the resulting packet depends on the event rate in the requested time interval. The "
			"resulting packet may be empty, if there is no event that happened in the requested interval.")
		.def(
			"numpy",
			[](const dv::DepthEventStore &self) {
				// Convert into a continuous memory buffer
				auto *events = new dv::DepthEventPacket(self.toPacket());

				// Deallocator
				py::capsule deallocate(events, [](void *ptr) {
					delete reinterpret_cast<dv::DepthEventPacket *>(ptr);
				});

				py::list names;
				names.append("timestamp");
				names.append("x");
				names.append("y");
				names.append("polarity");
				names.append("depth");
				py::list formats;
				formats.append("<i8");
				formats.append("<i2");
				formats.append("<i2");
				formats.append("<i1");
				formats.append("<u2");
				py::list offsets;
				offsets.append(0);
				offsets.append(8);
				offsets.append(10);
				offsets.append(12);
				// Polarity 1byte, +1 byte of padding between polarity and depth
				offsets.append(14);
				return py::array(py::dtype(names, formats, offsets, sizeof(dv::DepthEvent)), events->elements.size(),
					events->elements.data(), deallocate);
			},
			"Convert the event store into a array with named columns.")
		.def("toPacket", &dv::DepthEventStore::toPacket,
			"Convert event store into a continuous memory packet. This performs a deep copy of underlying data.")
		.def("__len__", &dv::DepthEventStore::size)
		.def("__getitem__", &dv::DepthEventStore::at)
		.def("__repr__",
			[](const dv::DepthEventStore &self) {
				std::stringstream ss;
				ss << self;
				return ss.str();
			})
		.def(
			"__iter__",
			[](const dv::DepthEventStore &self) {
				return py::make_iterator(self.begin(), self.end());
			},
			py::keep_alive<0, 1>());
}

/**
 * @brief Bind dv::EventStreamSlicer class to python
 * @param m pybind The module to bind to
 */
void bind_time_surface_base(py::module &m) {
	py::class_<dv::EventStreamSlicer>(m, "EventStreamSlicer")
		.def(py::init<>())
		.def(
			"doEveryNumberOfElements",
			[](dv::EventStreamSlicer &self, const size_t n, std::function<void(const dv::EventStore &)> cb) {
				return self.doEveryNumberOfElements(n, std::move(cb));
			},
			py::arg("n"), py::arg("callback"),
			"Adds a number-of-events triggered job to the Slicer. A job is defined by its interval and callback "
			"function. The slicer calls the callback function every `n` events, with the corresponding data. The (cpu) "
			"time interval between individual calls to the function depends on the physical event rate as well as the "
			"bulk sizes of the incoming data.")
		.def(
			"doEveryTimeInterval",
			[](dv::EventStreamSlicer &self, const dv::Duration time, std::function<void(const dv::EventStore &)> cb) {
				return self.doEveryTimeInterval(time, std::move(cb));
			},
			py::arg("time"), py::arg("callback"),
			"Adds an event-timestamp-interval triggered job to the Slicer. A job is defined by its interval and "
			"callback function. The slicer calls the callback whenever the timestamp difference of an incoming event "
			"to the last time the function was called is bigger than the interval. As the timing is based on event "
			"times rather than CPU time, the actual time periods are not guranteed, especially with a low event rate. "
			"The (cpu) time interval between individual calls to the function depends on the physical event rate as "
			"well as the bulk sizes of the incoming data.")
		.def("hasJob", &dv::EventStreamSlicer::hasJob, py::arg("job_id"),
			"Returns true if the slicer contains the slicejob with the provided id.")
		.def("removeJob", &dv::EventStreamSlicer::removeJob, py::arg("job_id"),
			"Removes the given job from the list of current jobs.")
		.def(
			"modifyTimeInterval",
			[](dv::EventStreamSlicer &self, const int jobId, const dv::Duration time) {
				self.modifyTimeInterval(jobId, time);
			},
			py::arg("job_id"), py::arg("timeInterval"),
			"Modifies the time interval of the supplied job to the requested value.")
		.def("modifyNumberInterval", &dv::EventStreamSlicer::modifyNumberInterval, py::arg("job_id"),
			py::arg("numberInterval"), "Modifies the number interval of the supplied job to the requested value.")
		.def(
			"accept",
			[](dv::EventStreamSlicer &self, const dv::EventStore &events) {
				self.accept(events);
			},
			py::arg("events"), "Adds full EventStore to the buffer and evaluates jobs.")
		.def(
			"accept",
			[](dv::EventStreamSlicer &self, const dv::Event &event) {
				self.accept(event);
			},
			py::arg("event"), "Adds a single event to the slicer buffer and evaluate jobs.");
}

/**
 * @brief Bind dv::TimeSurface class to python
 * @param m pybind The module to bind to
 */
void bind_time_surface(py::module &m) {
	py::class_<dv::TimeSurface>(m, "TimeSurface")
		.def(py::init<>(), "Dummy constructor Constructs a new, empty TimeSurface without any data allocated to it.")
		.def(py::init<uint32_t, uint32_t>(), py::arg("rows"), py::arg("cols"),
			"Creates a new TimeSurface with the given size. The data is zero initialized")
		.def(py::init<const cv::Size &>(), py::arg("resolution"),
			"Creates a new TimeSurface of the given size. The data is zero initialized.")
		.def("isEmpty", &dv::TimeSurface::isEmpty,
			"Returns true if the TimeSurface has zero size. In this case, it was not allocated with a size.")
		.def("cols", &dv::TimeSurface::cols, "Returns the number of columns of the TimeSurface")
		.def("rows", &dv::TimeSurface::rows, "Returns the number of rows of the TimeSurface")
		.def("size", &dv::TimeSurface::size, "The size of the TimeSurface.")
		.def("reset", &dv::TimeSurface::reset, "Sets all values in the time surface to zero.")
		.def("generateFrame", &dv::TimeSurface::generateFrame,
			"Generates a frame from the data contained in the event store")
		.def(
			"at",
			[](const dv::TimeSurface &self, const int16_t y, const int16_t x) -> int64_t {
				return self(y, x);
			},
			py::arg("y"), py::arg("x"), "Returns a value of the element at the given coordinates.")
		.def(
			"accept",
			[](dv::TimeSurface &self, const dv::EventStore &events) {
				return self.accept(events);
			},
			py::arg("events"), "Inserts the event store into the time surface.")
		.def(
			"accept",
			[](dv::TimeSurface &self, const dv::Event &event) {
				return self.accept(event);
			},
			py::arg("event"), "Inserts an event into the time surface.")
		.def(
			"subtract",
			[](dv::TimeSurface &self, const int64_t value) {
				self -= value;
			},
			py::arg("value"),
			"Subtracts a constant from the TimeSurface. Values are bounds checked to 0. If the new time would become "
			"negative, it is set to 0.")
		.def(
			"add",
			[](dv::TimeSurface &self, const int64_t value) {
				self += value;
			},
			py::arg("value"),
			"Adds a constant to the time surface. Values are bounds checked to 0. If the new time would become "
			"negative, it is set to 0.")
		.def(
			"setConstant",
			[](dv::TimeSurface &self, const int64_t value) {
				self = value;
			},
			py::arg("value"),
			"Assigns constant to the TimeSurface. Values are bounds checked to 0. If the new time would become "
			"negative, it is set to 0.")
		.def(
			"__getitem__",
			[](const dv::TimeSurface &self, const int16_t y, const int16_t x) -> int64_t {
				return self(y, x);
			},
			py::arg("y"), py::arg("x"), "Returns a value of the element at the given coordinates.");

	py::class_<dv::TimeSurfaceBase<dv::EventStore, uint8_t>> m_TimeSurface8U(m, "TimeSurface8U");
}

void bind_speed_invariant_time_surface(py::module &m) {
	py::class_<dv::SpeedInvariantTimeSurface>(m, "SpeedInvariantTimeSurface")
		.def(py::init<const cv::Size &>(), py::arg("resolution"))
		// Currently these bindings are bugged. Issue is open.
		//		.def("isEmpty", &dv::SpeedInvariantTimeSurface::isEmpty,
		//			"Returns true if the TimeSurface has zero size. In this case, it was not allocated with a size.")
		//		.def("cols", &dv::SpeedInvariantTimeSurface::cols, "Returns the number of columns of the TimeSurface")
		//		.def("rows", &dv::SpeedInvariantTimeSurface::rows, "Returns the number of rows of the TimeSurface")
		//		.def("size", &dv::SpeedInvariantTimeSurface::size, "The size of the TimeSurface.")
		.def("reset", &dv::SpeedInvariantTimeSurface::reset, "Sets all values in the time surface to zero.")
		.def("generateFrame", &dv::SpeedInvariantTimeSurface::generateFrame,
			"Generates a frame from the data contained in the event store")
		.def("at",
			[](const dv::SpeedInvariantTimeSurface &self, const int16_t y, const int16_t x) -> int64_t {
				return self(y, x);
			})
		.def(
			"at",
			[](const dv::SpeedInvariantTimeSurface &self, const int16_t y, const int16_t x) -> int64_t {
				return self(y, x);
			},
			py::arg("y"), py::arg("x"), "Returns a value of the element at the given coordinates.")
		.def(
			"accept",
			[](dv::SpeedInvariantTimeSurface &self, const dv::EventStore &events) {
				return self.accept(events);
			},
			py::arg("events"), "Inserts the event store into the time surface.")
		.def(
			"accept",
			[](dv::SpeedInvariantTimeSurface &self, const dv::Event &event) {
				return self.accept(event);
			},
			py::arg("event"), "Inserts an event into the time surface.")
		.def(
			"subtract",
			[](dv::SpeedInvariantTimeSurface &self, const int64_t value) {
				self -= value;
			},
			py::arg("value"),
			"Subtracts a constant from the TimeSurface. Values are bounds checked to 0. If the new time would become "
			"negative, it is set to 0.")
		.def(
			"add",
			[](dv::SpeedInvariantTimeSurface &self, const int64_t value) {
				self += value;
			},
			py::arg("value"),
			"Adds a constant to the time surface. Values are bounds checked to 0. If the new time would become "
			"negative, it is set to 0.")
		.def(
			"__getitem__",
			[](const dv::SpeedInvariantTimeSurface &self, const int16_t y, const int16_t x) -> int64_t {
				return self(y, x);
			},
			py::arg("y"), py::arg("x"), "Returns a value of the element at the given coordinates.");
}

/**
 * @brief Bind core modules to python
 * @param m pybind The module to bind to
 */
void bind_core(py::module &m) {
	bind_event_store(m);
	bind_depth_event_store(m);
	bind_time_surface_base(m);
	bind_time_surface(m);
	bind_speed_invariant_time_surface(m);
}
