#include <ros/ros.h>
#include <std_msgs/UInt16.h>
#include <std_msgs/Float32.h>
#include <std_srvs/Trigger.h>
#include <std_srvs/Empty.h>
#include <geometry_msgs/Pose.h>
#include <geometry_msgs/PoseArray.h>
#include <geometry_msgs/Twist.h>
#include <move_base_msgs/MoveBaseAction.h>
#include <actionlib_msgs/GoalStatusArray.h>
#include <actionlib/client/simple_action_client.h>
#include <tf/tf.h>
#include <tf/transform_listener.h>
#include <visualization_msgs/MarkerArray.h>

#include <yaml-cpp/yaml.h>
#include <waypoint_saver.h>  // include Waypoint class

#include <vector>
#include <fstream>
#include <string>
#include <exception>
#include <math.h>

#ifdef NEW_YAMLCPP
template<typename T>
void operator >> (const YAML::Node& node, T& i)
{
    i = node.as<T>();
}
#endif


class SwitchRunningStatus : public std::exception {
public:
    SwitchRunningStatus() : std::exception() { }
};



class WaypointsNavigation{
public:
    /*
    ++++++++++ Constructer ++++++++++
    */
    WaypointsNavigation() :
        has_activate_(false),
        stopped_(false),
        move_base_action_("move_base", true),
        rate_(10),
        last_moved_time_(0),
        dist_err_(1.0)
    {
        while((move_base_action_.waitForServer(ros::Duration(1.0)) == false) && (ros::ok() == true))
        {
            ROS_INFO("Waiting...");
        }
        ros::NodeHandle private_nh("~");
        private_nh.param("robot_frame", robot_frame_, std::string("base_link"));
        private_nh.param("world_frame", world_frame_, std::string("map"));
        
        double max_update_rate;
        private_nh.param("max_update_rate", max_update_rate, 10.0);
        rate_ = ros::Rate(max_update_rate);
        std::string filename = "";
        private_nh.param("filename", filename, filename);
        if (filename != "") {
            ROS_INFO_STREAM("Read waypoints data from " << filename);
            if (!readFile(filename)) {
                ROS_ERROR("Failed loading waypoints file");
            } else {
                finish_pose_ = waypoints_.poses.end()-1;
                computeWpOrientation();
            }
            current_waypoint_ = waypoints_.poses.begin();
            wp_num_.data = 1;
        } else {
            ROS_ERROR("waypoints file doesn't have name");
        }

        ros::NodeHandle nh;
        start_server_ = nh.advertiseService("start_wp_nav", &WaypointsNavigation::startNavigationCallback, this);
        stop_server_ = nh.advertiseService("stop_wp_nav", &WaypointsNavigation::stopNavigationCallback, this);
        resume_server_ = nh.advertiseService("resume_nav", &WaypointsNavigation::resumeNavigationCallback, this);
        cmd_vel_sub_ = nh.subscribe("cmd_vel", 1, &WaypointsNavigation::cmdVelCallback, this);
        wp_pub_ = nh.advertise<geometry_msgs::PoseArray>("waypoints", 10);
        max_vel_pub_ = nh.advertise<std_msgs::Float32>("max_vel", 5);
        wp_num_pub_ = nh.advertise<std_msgs::UInt16>("waypoint_number", 5);
        clear_costmaps_srv_ = nh.serviceClient<std_srvs::Empty>("/move_base/clear_costmaps");
    }



    /*
    ++++++++++ Read waypoints file ++++++++++
    */
    bool readFile(const std::string &filename)
    {
        waypoints_.poses.clear();
        try {
            std::ifstream ifs(filename.c_str(), std::ifstream::in);
            if (ifs.good() == false) return false;

            YAML::Node node;
            #ifdef NEW_YAMLCPP
                node = YAML::Load(ifs);
            #else
                YAML::Parser parser(ifs);
                parser.GetNextDocument(node);
            #endif

            // Read waypoints from yaml file
            #ifdef NEW_YAMLCPP
                const YAML::Node &wp_node_tmp = node["waypoints"];
                const YAML::Node *wp_node = wp_node_tmp ? &wp_node_tmp : NULL;
            #else
                const YAML::Node *wp_node = node.FindValue("waypoints");
            #endif
            geometry_msgs::Pose pose;
            Waypoint point;
            if (wp_node != NULL) {
                for(int i=0; i < wp_node->size(); i++) {
                    (*wp_node)[i]["point"]["x"] >> pose.position.x;
                    (*wp_node)[i]["point"]["y"] >> pose.position.y;
                    (*wp_node)[i]["point"]["z"] >> pose.position.z;
                    waypoints_.poses.push_back(pose);
                    point.x = pose.position.x;
                    point.y = pose.position.y;
                    point.z = pose.position.z;
                    (*wp_node)[i]["point"]["vel"] >> point.vel;
                    (*wp_node)[i]["point"]["rad"] >> point.rad;
                    (*wp_node)[i]["point"]["stop"] >> point.stop;
                    waypoint_list_.push_back(point);
                }
            } else {
                return false;
            }
            
            // Read finish_pose
            #ifdef NEW_YAMLCPP
                const YAML::Node &fp_node_tmp = node["finish_pose"];
                const YAML::Node *fp_node = fp_node_tmp ? &fp_node_tmp : NULL;
            #else
                const YAML::Node *fp_node = node.FindValue("finish_pose");
            #endif
            if (fp_node != NULL) {
                (*fp_node)["pose"]["position"]["x"] >> pose.position.x;
                (*fp_node)["pose"]["position"]["y"] >> pose.position.y;
                (*fp_node)["pose"]["position"]["z"] >> pose.position.z;
                (*fp_node)["pose"]["orientation"]["x"] >> pose.orientation.x;
                (*fp_node)["pose"]["orientation"]["y"] >> pose.orientation.y;
                (*fp_node)["pose"]["orientation"]["z"] >> pose.orientation.z;
                (*fp_node)["pose"]["orientation"]["w"] >> pose.orientation.w;
                waypoints_.poses.push_back(pose);
                point.x = pose.position.x;
                point.y = pose.position.y;
                point.z = pose.position.z;
                waypoint_list_.push_back(point);
            } else {
                return false;
            }

        } catch(YAML::ParserException &e) {
            return false;

        } catch(YAML::RepresentationException &e) {
            return false;
        }

        return true;
    }



    /*
    ++++++++++ Compute target orientation for each waypoint ++++++++++
    */
    void computeWpOrientation()
    {
        for(std::vector<geometry_msgs::Pose>::iterator it = waypoints_.poses.begin(); it != finish_pose_; it++) {
            double goal_direction = atan2((it+1)->position.y - (it)->position.y,
                                          (it+1)->position.x - (it)->position.x);
            (it)->orientation = tf::createQuaternionMsgFromYaw(goal_direction);
        }
        waypoints_.header.frame_id = world_frame_;
    }



    /*
    ++++++++++ Callback function for "StartWaypointsNavigation" button on rviz  ++++++++++
    */
    bool startNavigationCallback(std_srvs::Trigger::Request &request, std_srvs::Trigger::Response &response)
    {
        if ((has_activate_) || (current_waypoint_ != waypoints_.poses.begin())) {
            ROS_WARN("Waypoint navigation is already started");
            response.success = false;
            return false;
        }
        std_srvs::Empty empty;
        while(!clear_costmaps_srv_.call(empty)) {
            ROS_WARN("Resend clear costmap service");
            sleep();
        }
        current_waypoint_ = waypoints_.poses.begin();
        wp_num_.data = 1;
        has_activate_ = true;
        response.success = true;
        return true;
    }



    /*
    ++++++++++ Callback function for "ResumeWaypointsNavigation" button on rviz  ++++++++++
    */
    bool resumeNavigationCallback(std_srvs::Trigger::Request &request, std_srvs::Trigger::Response &response)
    {
        if (has_activate_) {
            ROS_WARN("Navigation is already active");
            response.success = false;
        } else {
            ROS_INFO("Navigation has resumed");
            has_activate_ = true;
            response.success = true;
        }
        return true;
    }



    /*
    ++++++++++ Callback function for service of "stop_wp_nav"  ++++++++++
    */
    bool stopNavigationCallback(std_srvs::Trigger::Request &request, std_srvs::Trigger::Response &response)
    {
        if (has_activate_) {
            ROS_INFO("Waypoint navigation has been stopped");
            has_activate_ = false;
            response.success = true;
        } else {
            ROS_WARN("Waypoint navigation is already stopped");
            response.success = false;
        }
        return true;
    }



    /*
    ++++++++++ Callback function for /cmd_vel topic  ++++++++++
    */
    void cmdVelCallback(const geometry_msgs::Twist &msg)
    {
        if (has_activate_ &&
            msg.linear.x > -0.001 && msg.linear.x < 0.001 &&
            msg.linear.y > -0.001 && msg.linear.y < 0.001 &&
            msg.linear.z > -0.001 && msg.linear.z < 0.001 &&
            msg.angular.x > -0.001 && msg.angular.x < 0.001 &&
            msg.angular.y > -0.001 && msg.angular.y < 0.001 &&
            msg.angular.z > -0.001 && msg.angular.z < 0.001)
        {
            // ROS_INFO("command velocity all zero");
        } else {
            last_moved_time_ = ros::Time::now().toSec();
        }
    }



    /*
    ++++++++++ Check if robot reached the goal sent to move_base ++++++++++
    */
    bool navigationFinished()
    {
        return move_base_action_.getState() == actionlib::SimpleClientGoalState::SUCCEEDED;
    }



    /*
    ++++++++++ Check if robot reached current waypoint ++++++++++
    */
    bool onNavigationPoint(const geometry_msgs::Point &dest, double dist_err=1.0)
    {
        if (waypoint_list_[wp_num_.data-1].stop) {
            return navigationFinished();
        }
        tf::StampedTransform robot_gl = getRobotPosGL();
        const double wx = dest.x;
        const double wy = dest.y;
        const double rx = robot_gl.getOrigin().x();
        const double ry = robot_gl.getOrigin().y();
        const double dist = std::sqrt(std::pow(wx - rx, 2) + std::pow(wy - ry, 2));
        return dist < dist_err;
    }



    /*
    ++++++++++ Get gloabl position of robot ++++++++++
    */
    tf::StampedTransform getRobotPosGL()
    {
        tf::StampedTransform robot_gl;
        try {
            tf_listener_.lookupTransform(world_frame_, robot_frame_, ros::Time(0.0), robot_gl);
        } catch(tf::TransformException &e) {
            ROS_WARN_STREAM("tf::TransformException: " << e.what());
        }
        return robot_gl;
    }



    /*
    ++++++++++ Sleep function used in main loop function ++++++++++
    */
    void sleep()
    {
        rate_.sleep();
        ros::spinOnce();
        publishPoseArray();
    }



    /*
    ++++++++++ Publish waypoints to be displayed as arrows on rviz ++++++++++
    */
    void publishPoseArray()
    {
        waypoints_.header.stamp = ros::Time::now();
        wp_pub_.publish(waypoints_);
    }



    /*
    ++++++++++ Send goal to move_base ++++++++++
    */
    void startNavigationGL(const geometry_msgs::Pose &dest)
    {
        setMaxVel();
        move_base_msgs::MoveBaseGoal move_base_goal;
        move_base_goal.target_pose.header.stamp = ros::Time::now();
        move_base_goal.target_pose.header.frame_id = world_frame_;
        move_base_goal.target_pose.pose.position = dest.position;
        move_base_goal.target_pose.pose.orientation = dest.orientation;
        move_base_action_.sendGoal(move_base_goal);
    }



    /*
    ++++++++++ Publish ratio to maximum speed ++++++++++
    */
    void setMaxVel()
    {
        float max_vel_rate = waypoint_list_[wp_num_.data-1].vel;
        max_vel_msg_.data = max_vel_rate;
        max_vel_pub_.publish(max_vel_msg_);
    }



    /*
    ++++++++++ Main loop function ++++++++++
    */
    void run()
    {
        ROS_INFO("Waiting for waypoint navigation to start.");
        while(ros::ok()) {
            // has_activate_ is false, nothing to do
            if (!has_activate_) {
                sleep();
                continue;
            }
            // go to fianal goal and finish process
            if (current_waypoint_ == finish_pose_) {
                ROS_INFO_STREAM("Go to final goal");
                startNavigationGL(*current_waypoint_);
                while(!navigationFinished() && ros::ok()) sleep();
                ROS_INFO("Final goal reached!!");
                has_activate_ = false;
                continue;
            }
            // go to current waypoint
            ROS_INFO_STREAM("Go to waypoint " << wp_num_.data);
            ROS_INFO_STREAM("x: " << waypoint_list_[wp_num_.data-1].x << ", y: " << waypoint_list_[wp_num_.data-1].y);
            startNavigationGL(*current_waypoint_);
            int resend_goal = 0;
            double start_nav_time = ros::Time::now().toSec();
            dist_err_ = waypoint_list_[wp_num_.data-1].rad;
            try {
                // loop until reach waypoint
                while(!onNavigationPoint(current_waypoint_->position, dist_err_)) {
                    if (!has_activate_) throw SwitchRunningStatus();
                    if ((!waypoint_list_[wp_num_.data-1].stop) && (navigationFinished())) {
                        ROS_WARN("Reached default yaw_goal_torelance.");
                        break;
                    }
                    double time = ros::Time::now().toSec();
                    if (time - start_nav_time > 10.0 && time - last_moved_time_ > 10.0) {
                        ROS_WARN("Resend the navigation goal.");
                        std_srvs::Empty empty;
                        clear_costmaps_srv_.call(empty);
                        startNavigationGL(*current_waypoint_);
                        resend_goal++;
                        if (resend_goal == 3) {
                            ROS_WARN("Skip waypoint.");
                            break;
                        }
                        start_nav_time = time;
                    }
                    wp_num_pub_.publish(wp_num_);
                    sleep();
                }
                // if current waypoint is stop point
                if (waypoint_list_[wp_num_.data-1].stop) {
                    has_activate_ = false;
                    move_base_action_.cancelAllGoals();
                    ROS_INFO("Waiting for navigation to resume...");
                }
                // update current waypoint
                current_waypoint_++;
                wp_num_.data++;

            } catch(const SwitchRunningStatus &e) {
                ROS_INFO_STREAM("Running status switched");
            }
            sleep();
        }
    }




private:
    actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> move_base_action_;
    geometry_msgs::PoseArray waypoints_;
    visualization_msgs::MarkerArray marker_;
    std::vector<Waypoint> waypoint_list_;
    std::vector<geometry_msgs::Pose>::iterator current_waypoint_;
    std::vector<geometry_msgs::Pose>::iterator finish_pose_;
    bool has_activate_, stopped_;
    std::string robot_frame_, world_frame_;
    tf::TransformListener tf_listener_;
    ros::Rate rate_;
    ros::ServiceServer start_server_, stop_server_, resume_server_;
    ros::Subscriber cmd_vel_sub_, move_base_status_sub_;
    ros::Publisher wp_pub_, max_vel_pub_, wp_num_pub_;
    ros::ServiceClient clear_costmaps_srv_;
    double last_moved_time_, dist_err_;
    std_msgs::UInt16 wp_num_;
    std_msgs::Float32 max_vel_msg_;
};






int main(int argc, char *argv[]) {
    ros::init(argc, argv, ROS_PACKAGE_NAME);
    WaypointsNavigation w_nav;
    w_nav.run();

    return 0;
}
