/*
 * Detecting and pose estimation of ArUco markers
 * Copyright (C) 2018 Copter Express Technologies
 *
 * Author: Oleg Kalachev <okalachev@gmail.com>
 *
 * Distributed under MIT License (available at https://opensource.org/licenses/MIT).
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 */

/*
 * Code is based on https://github.com/UbiquityRobotics/fiducials, which is distributed
 * under the BSD license.
 */

#include <math.h>
#include <vector>
#include <string>
#include <ros/ros.h>
#include <nodelet/nodelet.h>
#include <pluginlib/class_list_macros.h>
#include <tf/transform_datatypes.h>
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>
#include <tf2_ros/transform_broadcaster.h>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <geometry_msgs/Vector3.h>
#include <geometry_msgs/Pose.h>
#include <geometry_msgs/PoseStamped.h>
#include <geometry_msgs/PoseWithCovarianceStamped.h>
#include <geometry_msgs/TransformStamped.h>
#include <visualization_msgs/Marker.h>
#include <visualization_msgs/MarkerArray.h>

#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/aruco.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>

#include <aruco_pose/Marker.h>
#include <aruco_pose/MarkerArray.h>

#include "utils.h"

using std::vector;
using cv::Mat;

class ArucoDetect : public nodelet::Nodelet {
private:
	ros::NodeHandle nh_, nh_priv_;
	tf2_ros::TransformBroadcaster br_;
	tf2_ros::Buffer tf_buffer_;
	tf2_ros::TransformListener tf_listener_{tf_buffer_};
	cv::Ptr<cv::aruco::Dictionary> dictionary_;
	cv::Ptr<cv::aruco::DetectorParameters> parameters_;
	image_transport::Publisher debug_pub_;
	image_transport::CameraSubscriber img_sub_;
	ros::Publisher markers_pub_, vis_markers_pub_;
	bool estimate_poses_, send_tf_;
	double length_;
	std::string frame_id_prefix_, snap_orientation_;
	Mat camera_matrix_, dist_coeffs_;
	aruco_pose::MarkerArray array_;
	visualization_msgs::MarkerArray vis_array_;

public:
	virtual void onInit()
	{
		nh_ = getNodeHandle();
		nh_priv_ = getPrivateNodeHandle();

		int dictionary;
		nh_priv_.param("dictionary", dictionary, 2);
		nh_priv_.param("estimate_poses", estimate_poses_, true);
		nh_priv_.param("send_tf", send_tf_, true);
		if (estimate_poses_ && !nh_priv_.getParam("length", length_)) {
			ROS_FATAL("aruco_detect: can't estimate marker's poses as ~length parameter is not defined");
			ros::shutdown();
		}
		nh_priv_.param<std::string>("snap_orientation", snap_orientation_, "");
		nh_priv_.param<std::string>("frame_id_prefix", frame_id_prefix_, "aruco_");

		camera_matrix_ = cv::Mat::zeros(3, 3, CV_64F);
		dist_coeffs_ = cv::Mat::zeros(8, 1, CV_64F);

		dictionary_ = cv::aruco::getPredefinedDictionary(static_cast<cv::aruco::PREDEFINED_DICTIONARY_NAME>(dictionary));
		parameters_ = cv::aruco::DetectorParameters::create();
		parameters_->cornerRefinementMethod = cv::aruco::CORNER_REFINE_SUBPIX;

		image_transport::ImageTransport it(nh_);
		image_transport::ImageTransport it_priv(nh_priv_);

		debug_pub_ = it_priv.advertise("debug", 1);
		markers_pub_ = nh_priv_.advertise<aruco_pose::MarkerArray>("markers", 1);
		vis_markers_pub_ = nh_priv_.advertise<visualization_msgs::MarkerArray>("visualization", 1);
		img_sub_ = it.subscribeCamera("image_raw", 1, &ArucoDetect::imageCallback, this);

		ROS_INFO("aruco_detect: ready");
	}

private:
	void imageCallback(const sensor_msgs::ImageConstPtr& msg, const sensor_msgs::CameraInfoConstPtr &cinfo)
	{
		Mat image = cv_bridge::toCvShare(msg, "bgr8")->image;

		vector<int> ids;
		vector<vector<cv::Point2f>> corners, rejected;
		vector<cv::Vec3d> rvecs, tvecs;
		vector<cv::Point3f> obj_points;
		geometry_msgs::TransformStamped snap_to;

		// Detect markers
		cv::aruco::detectMarkers(image, dictionary_, corners, ids, parameters_, rejected);

		array_.header.stamp = msg->header.stamp;
		array_.header.frame_id = msg->header.frame_id;
		array_.markers.clear();

		if (ids.size() != 0) {
			parseCameraInfo(cinfo, camera_matrix_, dist_coeffs_);

			// Estimate individual markers' poses
			if (estimate_poses_) {
				cv::aruco::estimatePoseSingleMarkers(corners, length_, camera_matrix_, dist_coeffs_,
				                                     rvecs, tvecs);

				if (!snap_orientation_.empty()) {
					try {
						snap_to = tf_buffer_.lookupTransform(msg->header.frame_id, snap_orientation_,
						                                     msg->header.stamp, ros::Duration(0.02));
					} catch (const tf2::TransformException& e) {
						ROS_WARN_THROTTLE(5, "aruco_detect: can't snap: %s", e.what());
					}
				}
			}

			array_.markers.reserve(ids.size());
			aruco_pose::Marker marker;
			geometry_msgs::TransformStamped transform;
			transform.header.stamp = msg->header.stamp;
			transform.header.frame_id = msg->header.frame_id;

			for (unsigned int i = 0; i < ids.size(); i++) {
				marker.id = ids[i];
				fillCorners(marker, corners[i]);

				if (estimate_poses_) {
					fillPose(marker.pose.pose, rvecs[i], tvecs[i]);

					// snap orientation (if enabled and snap frame avaiable)
					if (!snap_orientation_.empty() && !snap_to.header.frame_id.empty()) {
						snapOrientation(marker.pose.pose.orientation, snap_to.transform.rotation);
					}

					// TODO: check IDs are unique
					if (send_tf_) {
						transform.child_frame_id = getChildFrameId(ids[i]);
						transform.transform.rotation = marker.pose.pose.orientation;
						fillTranslation(transform.transform.translation, tvecs[i]);
						br_.sendTransform(transform);
					}
				}
				array_.markers.push_back(marker);
			}
		}

		markers_pub_.publish(array_);

		// Publish visualization markers
		if (estimate_poses_ && vis_markers_pub_.getNumSubscribers() != 0) {
			// Delete all markers
			visualization_msgs::Marker vis_marker;
			vis_marker.action = visualization_msgs::Marker::DELETEALL;
			vis_array_.markers.clear();
			vis_array_.markers.reserve(ids.size() + 1);
			vis_array_.markers.push_back(vis_marker);

			for (unsigned int i = 0; i < ids.size(); i++)
				pushVisMarkers(msg->header.frame_id, msg->header.stamp, array_.markers[i].pose.pose,
				               length_, ids[i], i);

			vis_markers_pub_.publish(vis_array_);
		}

		// Publish debug image
		if (debug_pub_.getNumSubscribers() != 0) {
			Mat debug = image.clone();
			cv::aruco::drawDetectedMarkers(debug, corners, ids); // draw markers
			if (estimate_poses_)
				for (unsigned int i = 0; i < ids.size(); i++)
					cv::aruco::drawAxis(debug, camera_matrix_, dist_coeffs_, rvecs[i], tvecs[i], length_);

			cv_bridge::CvImage out_msg;
			out_msg.header.frame_id = msg->header.frame_id;
			out_msg.header.stamp = msg->header.stamp;
			out_msg.encoding = sensor_msgs::image_encodings::BGR8;
			out_msg.image = debug;
			debug_pub_.publish(out_msg.toImageMsg());
		}
	}

	inline void fillCorners(aruco_pose::Marker& marker, const vector<cv::Point2f>& corners) const
	{
		marker.c1.x = corners[0].x;
		marker.c2.x = corners[1].x;
		marker.c3.x = corners[2].x;
		marker.c4.x = corners[3].x;
		marker.c1.y = corners[0].y;
		marker.c2.y = corners[1].y;
		marker.c3.y = corners[2].y;
		marker.c4.y = corners[3].y;
	}

	inline void fillPose(geometry_msgs::Pose& pose, const cv::Vec3d& rvec, const cv::Vec3d& tvec) const
	{
		pose.position.x = tvec[0];
		pose.position.y = tvec[1];
		pose.position.z = tvec[2];

		double angle = norm(rvec);
		cv::Vec3d axis = rvec / angle;

		tf2::Quaternion q;
		q.setRotation(tf2::Vector3(axis[0], axis[1], axis[2]), angle);

		pose.orientation.w = q.w();
		pose.orientation.x = q.x();
		pose.orientation.y = q.y();
		pose.orientation.z = q.z();
	}

	inline void fillTranslation(geometry_msgs::Vector3& translation, const cv::Vec3d& tvec) const
	{
		translation.x = tvec[0];
		translation.y = tvec[1];
		translation.z = tvec[2];
	}

	void pushVisMarkers(const std::string& frame_id, const ros::Time& stamp,
	                    const geometry_msgs::Pose &pose, double length, int id, int index)
	{
		visualization_msgs::Marker marker;
		marker.header.frame_id = frame_id;
		marker.header.stamp = stamp;
		marker.action = visualization_msgs::Marker::ADD;
		marker.id = index;

		// Marker
		marker.ns = "aruco_marker";
		marker.type = visualization_msgs::Marker::CUBE;
		marker.scale.x = length;
		marker.scale.y = length;
		marker.scale.z = 0.001;
		marker.color.r = 1;
		marker.color.g = 1;
		marker.color.b = 1;
		marker.color.a = 0.9;
		marker.pose = pose;
		vis_array_.markers.push_back(marker);

		// Label
		marker.ns = "aruco_marker_label";
		marker.type = visualization_msgs::Marker::TEXT_VIEW_FACING;
		marker.scale.z = length * 0.6;
		marker.color.r = 0;
		marker.color.g = 0;
		marker.color.b = 0;
		marker.color.a = 1;
		marker.text = std::to_string(id);
		marker.pose = pose;
		vis_array_.markers.push_back(marker);
	}

	inline std::string getChildFrameId(int id) const
	{
		return frame_id_prefix_ + std::to_string(id);
	}
};

PLUGINLIB_EXPORT_CLASS(ArucoDetect, nodelet::Nodelet)
