#pragma once

#include "../../../include/dv-processing/camera/camera_geometry.hpp"

#include <pybind11/pybind11.h>

namespace py = pybind11;

/**
 * @brief Bind camera::DistortionModel, CameraGeometry, stringToDistortionModel() and distortionModelToString() to
 * python
 * @param m pybind The module to bind to
 */
void bind_camera_geometry(py::module &m) {
	using pybind11::operator""_a;

	py::enum_<dv::camera::DistortionModel>(m, "DistortionModel")
		.value("NONE", dv::camera::DistortionModel::NONE)
		.value("RADIAL_TANGENTIAL", dv::camera::DistortionModel::RADIAL_TANGENTIAL)
		.value("EQUIDISTANT", dv::camera::DistortionModel::EQUIDISTANT);

	m.def("stringToDistortionModel", &dv::camera::stringToDistortionModel, "modelName"_a,
		"Convert a string into the Enum of the DistortionModel");
	m.def("distortionModelToString", &dv::camera::distortionModelToString, "model"_a,
		"Convert a DistortionModel Enum into a string");

	py::class_<dv::camera::CameraGeometry, std::shared_ptr<dv::camera::CameraGeometry>>(m, "CameraGeometry")
		.def(py::init<const float, const float, const float, const float, const cv::Size &>(), py::arg("fx"),
			py::arg("fy"), py::arg("cx"), py::arg("cy"), py::arg("resolution"),
			"Create a camera geometry model without distortion model. Currently only radial tangential model is "
			"supported. Any calls to function dependent on distortion will cause exceptions or segfaults.")
		.def(py::init<const std::vector<float> &, const float, const float, const float, const float, const cv::Size &,
				 const dv::camera::DistortionModel>(),
			py::arg("distortion"), py::arg("fx"), py::arg("fy"), py::arg("cx"), py::arg("cy"), py::arg("resolution"),
			py::arg("distortionModel"),
			"Create a camera geometry model with distortion model. Currently only radial tangential model is "
			"supported.")
		.def("undistort", &dv::camera::CameraGeometry::undistort<Eigen::Vector2f, Eigen::Vector2f>, py::arg("point"),
			"Returns pixel coordinates of given point with applied back projection, undistortion, and projection. This "
			"function uses look-up table and is designed for minimal execution speed. WARNING: will cause a segfault "
			"if coordinates are out-of-bounds or if distortion model is not available.")
		.def("undistortEvents", &dv::camera::CameraGeometry::undistortEvents, py::arg("events"),
			"Undistort event coordinates, discards events which fall beyond camera resolution.")
		.def("undistortSequence",
			&dv::camera::CameraGeometry::undistortSequence<std::vector<Eigen::Vector2f>, std::vector<Eigen::Vector3f>>,
			py::arg("points"), "Undistort point coordinates.")
		.def("distort", &dv::camera::CameraGeometry::distort<Eigen::Vector3f, Eigen::Vector3f>, py::arg("point"),
			"Apply distortion to a 3D point.")
		.def("distortSequence",
			&dv::camera::CameraGeometry::distortSequence<std::vector<Eigen::Vector3f>, std::vector<Eigen::Vector3f>>,
			py::arg("points"), "Apply direct distortion on the 3D points.")

		.def(
			"backProject",
			[](const dv::camera::CameraGeometry &self, const Eigen::Vector2f &point,
				const bool subPixel = false) -> Eigen::Vector3f {
				if (subPixel) {
					return self.backProject<Eigen::Vector3f, Eigen::Vector2f,
						dv::camera::CameraGeometry::FunctionImplementation::SUB_PIXEL>(point);
				}
				else {
					return self.backProject<Eigen::Vector3f, Eigen::Vector2f,
						dv::camera::CameraGeometry::FunctionImplementation::LUT>(point);
				}
			},
			"point"_a, "subPixel"_a = false,
			"Back-project pixel coordinates into a plane at depth = 1.0 meters from focal point. If subPixel argument "
			"is False - the function just performs lookup-table operation within pixel bound, rounding the coordinates "
			"to first integer. If subPixel is set to True, it will perform the precise math operations")
		.def(
			"backProjectSequence",
			[](const dv::camera::CameraGeometry &self, const std::vector<Eigen::Vector2f> &points,
				const bool subPixel = false) -> std::vector<Eigen::Vector3f> {
				if (subPixel) {
					return self.backProjectSequence<std::vector<Eigen::Vector3f>, std::vector<Eigen::Vector2f>,
						dv::camera::CameraGeometry::FunctionImplementation::SUB_PIXEL>(points);
				}
				else {
					return self.backProjectSequence<std::vector<Eigen::Vector3f>, std::vector<Eigen::Vector2f>,
						dv::camera::CameraGeometry::FunctionImplementation::LUT>(points);
				}
			},
			"points"_a, "subPixel"_a = false,
			"Back-project a sequence of pixel coordinates into rays at a plane at depth = 1.0 meters from focal point. "
			"If subPixel argument "
			"is False - the function just performs lookup-table operation within pixel bound, rounding the coordinates "
			"to first integer. If subPixel is set to True, it will perform the precise math operations")
		.def(
			"backProjectSequence",
			[](const dv::camera::CameraGeometry &self, const dv::EventStore &points,
				const bool subPixel = false) -> std::vector<Eigen::Vector3f> {
				if (subPixel) {
					return self.backProjectSequence<std::vector<Eigen::Vector3f>, dv::EventStore,
						dv::camera::CameraGeometry::FunctionImplementation::SUB_PIXEL>(points);
				}
				else {
					return self.backProjectSequence<std::vector<Eigen::Vector3f>, dv::EventStore,
						dv::camera::CameraGeometry::FunctionImplementation::LUT>(points);
				}
			},
			"events"_a, "subPixel"_a = false,
			"Back-project a sequence of pixel coordinates into rays at a plane at depth = 1.0 meters from focal point. "
			"If subPixel argument "
			"is False - the function just performs lookup-table operation within pixel bound, rounding the coordinates "
			"to first integer. If subPixel is set to True, it will perform the precise math operations")
		.def("backProjectUndistort",
			&dv::camera::CameraGeometry::backProjectUndistort<Eigen::Vector3f, Eigen::Vector2f>, py::arg("point"),
			"Returns a unit ray of given coordinates with applied back projection and undistortion. This function uses "
			"look-up table and is designed for minimal execution speed. WARNING: will cause a segfault if coordinates "
			"are out-of-bounds or if distortion model is not available.")
		.def("backProjectUndistortSequence",
			&dv::camera::CameraGeometry::backProjectUndistortSequence<std::vector<Eigen::Vector3f>,
				std::vector<Eigen::Vector2f>>,
			py::arg("points"),
			"Undistort and back project a batch of points. Output is normalized point coordinates as unit rays.")
		.def("project", &dv::camera::CameraGeometry::project<Eigen::Vector2f, Eigen::Vector3f>, py::arg("point"),
			"Project a 3D point into pixel plane. WARNING: Does not perform range checking!")
		.def("projectSequence",
			&dv::camera::CameraGeometry::projectSequence<std::vector<Eigen::Vector2f>, std::vector<Eigen::Vector3f>>,
			py::arg("points"), py::arg("dimensionCheck") = true, "Project a batch of 3D points into pixel plane.")
		.def("isWithinDimensions", &dv::camera::CameraGeometry::isWithinDimensions<Eigen::Vector2f>, py::arg("point"),
			"Check whether given coordinates are within valid range.")
		.def("isUndistortionAvailable", &dv::camera::CameraGeometry::isUndistortionAvailable,
			"Checks whether this camera geometry calibration contains coefficient for an undistortion model.")
		.def(
			"getCameraMatrix",
			[](const dv::camera::CameraGeometry &self) -> Eigen::Matrix3f {
				Eigen::Matrix3f mat;
				cv::cv2eigen(self.getCameraMatrix(), mat);
				return mat;
			},
			"Get camera matrix in the format: | mFx 0 mCx | | 0 mFy mCy | | 0 0 1 |")
		.def("getFocalLength", &dv::camera::CameraGeometry::getFocalLength<Eigen::Vector2f>, "Focal length in pixels.")
		.def("getCentralPoint", &dv::camera::CameraGeometry::getCentralPoint<Eigen::Vector2f>,
			"Central point coordinates.")
		.def("getDistortion", &dv::camera::CameraGeometry::getDistortion, "Get distortion coefficients.")
		.def("getDistortionModel", &dv::camera::CameraGeometry::getDistortionModel, "Get distortion model.")
		.def("getResolution", &dv::camera::CameraGeometry::getResolution, "Get the camera resolution.");
}
