#!/usr/bin/env python
from __future__ import division

import rospy
from geometry_msgs.msg import TransformStamped, PoseStamped, Point, PointStamped, Vector3, Vector3Stamped, TwistStamped, QuaternionStamped
from sensor_msgs.msg import NavSatFix, BatteryState
import tf2_ros
import tf2_geometry_msgs
from mavros_msgs.msg import PositionTarget, AttitudeTarget, State
from mavros_msgs.srv import CommandBool, SetMode
from threading import Lock
import math

from global_local import global_to_local
from util import euler_from_orientation, vector3_from_point, orientation_from_euler
from std_srvs.srv import Trigger
from clever import srv


rospy.init_node('simple_offboard')


# TF2 stuff
tf_broadcaster = tf2_ros.TransformBroadcaster()
static_tf_broadcaster = tf2_ros.StaticTransformBroadcaster()

tf_buffer = tf2_ros.Buffer()
tf_listener = tf2_ros.TransformListener(tf_buffer)


position_pub = rospy.Publisher('/mavros/setpoint_raw/local', PositionTarget, queue_size=1)
attitude_pub = rospy.Publisher('/mavros/setpoint_raw/attitude', AttitudeTarget, queue_size=1)
target_pub = rospy.Publisher('~target', PoseStamped, queue_size=1)

arming = rospy.ServiceProxy('/mavros/cmd/arming', CommandBool, persistent=True)
set_mode = rospy.ServiceProxy('/mavros/set_mode', SetMode, persistent=True)


pose = None
global_position = None
velocity = None
state = None
battery = None


def pose_update(data):
    global pose
    pose = data


def global_position_update(data):
    global global_position
    global_position = data


def velocity_update(data):
    global velocity
    velocity = data


def state_update(data):
    global state
    state = data


def battery_update(data):
    global battery
    battery = data


rospy.Subscriber('/mavros/state', State, state_update)
rospy.Subscriber('/mavros/local_position/pose', PoseStamped, pose_update)
rospy.Subscriber('/mavros/local_position/velocity', TwistStamped, velocity_update)
rospy.Subscriber('/mavros/global_position/global', NavSatFix, global_position_update)
rospy.Subscriber('/mavros/battery', BatteryState, battery_update)


AUTO_OFFBOARD = rospy.get_param('~auto_offboard', True)
AUTO_ARM = AUTO_OFFBOARD and rospy.get_param('~auto_arm', True)
OFFBOARD_TIMEOUT = rospy.Duration(rospy.get_param('~offboard_timeout', 3))
ARM_TIMEOUT = rospy.Duration(rospy.get_param('~arm_timeout', 5))
NAVIGATE_AFTER_ARMED = rospy.Duration(rospy.get_param('~navigate_after_armed', False))
TRANSFORM_TIMEOUT = rospy.Duration(rospy.get_param('~transform_timeout', 3))
SETPOINT_RATE = rospy.get_param('~setpoint_rate', 30)
LOCAL_FRAME = rospy.get_param('~local_frame', 'local_origin')
LAND_MODE = rospy.get_param('~land_mode', 'AUTO.LAND')
LAND_TIMEOUT = rospy.Duration(rospy.get_param('~land_timeout', 2))


def offboard_and_arm():
    if AUTO_OFFBOARD and state.mode != 'OFFBOARD':
        rospy.sleep(.3)
        rospy.loginfo('Switch mode to OFFBOARD')
        res = set_mode(base_mode=0, custom_mode='OFFBOARD')

        start = rospy.get_rostime()
        while True:
            if state.mode == 'OFFBOARD':
                break
            if rospy.get_rostime() - start > OFFBOARD_TIMEOUT:
                raise Exception('OFFBOARD request timed out')
            rospy.sleep(0.1)

    if AUTO_ARM and not state.armed:
        rospy.loginfo('Arming')
        res = arming(True)

        start = rospy.get_rostime()
        while True:
            if state.armed:
                return True
            if rospy.get_rostime() - start > ARM_TIMEOUT:
                raise Exception('Arming timed out')
            rospy.sleep(0.1)


ps = PoseStamped()
vs = Vector3Stamped()


BRAKE_TIME = rospy.Duration(0)


def get_navigate_setpoint(stamp, start, finish, start_stamp, speed):
    distance = math.sqrt((finish.z - start.z)**2 + (finish.x - start.x)**2 + (finish.y - start.y)**2)
    time = rospy.Duration(distance / speed)
    k = (stamp - start_stamp) / time
    time_left = start_stamp + time - stamp

    if BRAKE_TIME and time_left < BRAKE_TIME:
        # time to brake
        time_before_braking = time - BRAKE_TIME
        brake_time_passed = (stamp - start_stamp - time_before_braking)

        if brake_time_passed > 2 * BRAKE_TIME:
            # finish
            k = 1
        else:
            # brake!
            k_before_braking = time_before_braking / time
            k_after_braking = (speed * brake_time_passed.to_sec() - brake_time_passed.to_sec() ** 2 * speed / 4 / BRAKE_TIME.to_sec()) / distance
            k = k_before_braking + k_after_braking

    k = min(k, 1)

    p = Point()
    p.x = start.x + (finish.x - start.x) * k
    p.y = start.y + (finish.y - start.y) * k
    p.z = start.z + (finish.z - start.z) * k
    return p


def get_publisher_and_message(req, stamp, continued=True, update_frame=True):
    ps.header.stamp = stamp
    vs.header.stamp = stamp

    if isinstance(req, (srv.NavigateRequest, srv.NavigateGlobalRequest)):
        global current_nav_start, current_nav_start_stamp, current_nav_finish

        if update_frame:
            ps.header.frame_id = req.frame_id or LOCAL_FRAME
            ps.pose.position = Point(getattr(req, 'x', 0), getattr(req, 'y', 0), req.z)
            ps.pose.orientation = orientation_from_euler(0, 0, req.yaw)
            current_nav_finish = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)

            if isinstance(req, srv.NavigateGlobalRequest):
                # Recalculate x and y from lat and lon
                current_nav_finish.pose.position.x, current_nav_finish.pose.position.y = \
                    global_to_local(req.lat, req.lon)

        if not continued:
            current_nav_start = pose.pose.position
            current_nav_start_stamp = stamp

        if NAVIGATE_AFTER_ARMED and not state.armed:
            current_nav_start_stamp = stamp

        setpoint = get_navigate_setpoint(stamp, current_nav_start, current_nav_finish.pose.position,
                                         current_nav_start_stamp, req.speed)

        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW_RATE,
                             position=setpoint,
                             yaw=euler_from_orientation(current_nav_finish.pose.orientation)[2] - math.pi / 2)
        return position_pub, msg

    elif isinstance(req, srv.SetPositionRequest):
        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.position = Point(req.x, req.y, req.z)
        ps.pose.orientation = orientation_from_euler(0, 0, req.yaw)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)

        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW_RATE,
                             position=pose_local.pose.position,
                             yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2)
        return position_pub, msg

    elif isinstance(req, srv.SetPositionYawRateRequest):
        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.position = Point(req.x, req.y, req.z)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW,
                             position=pose_local.pose.position,
                             yaw_rate=req.yaw_rate)
        return position_pub, msg

    elif isinstance(req, srv.SetPositionGlobalRequest):
        x, y = global_to_local(req.lat, req.lon)

        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.position = Point(0, 0, req.z)
        ps.pose.orientation = orientation_from_euler(0, 0, req.yaw)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        pose_local.pose.position.x = x
        pose_local.pose.position.y = y

        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW_RATE,
                             position=pose_local.pose.position,
                             yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2)
        return position_pub, msg

    elif isinstance(req, srv.SetPositionGlobalYawRateRequest):
        x, y = global_to_local(req.lat, req.lon)

        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.position = Point(0, 0, req.z)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        pose_local.pose.position.x = x
        pose_local.pose.position.y = y

        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW,
                             position=pose_local.pose.position,
                             yaw_rate=req.yaw_rate)
        return position_pub, msg

    elif isinstance(req, srv.SetVelocityRequest):
        vs.vector = Vector3(req.vx, req.vy, req.vz)
        vs.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.orientation = orientation_from_euler(0, 0, req.yaw)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        vector_local = tf_buffer.transform(vs, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_PX + PositionTarget.IGNORE_PY + PositionTarget.IGNORE_PZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW_RATE,
                             velocity=vector_local.vector,
                             yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2)
        return position_pub, msg

    elif isinstance(req, srv.SetVelocityYawRateRequest):
        vs.vector = Vector3(req.vx, req.vy, req.vz)
        vs.header.frame_id = req.frame_id or LOCAL_FRAME
        vector_local = tf_buffer.transform(vs, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED,
                             type_mask=PositionTarget.IGNORE_PX + PositionTarget.IGNORE_PY + PositionTarget.IGNORE_PZ +
                                       PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ +
                                       PositionTarget.IGNORE_YAW,
                             velocity=vector_local.vector,
                             yaw_rate=req.yaw_rate)
        return position_pub, msg

    elif isinstance(req, srv.SetAttitudeRequest):
        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.orientation = orientation_from_euler(req.roll, req.pitch, req.yaw)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        msg = AttitudeTarget(orientation=pose_local.pose.orientation,
                             thrust=req.thrust,
                             type_mask=AttitudeTarget.IGNORE_YAW_RATE + AttitudeTarget.IGNORE_PITCH_RATE +
                                       AttitudeTarget.IGNORE_ROLL_RATE)
        return attitude_pub, msg

    elif isinstance(req, srv.SetAttitudeYawRateRequest):
        msg = AttitudeTarget(orientation=orientation_from_euler(req.roll, req.pitch, 0),
                             thrust=req.thrust,
                             type_mask=AttitudeTarget.IGNORE_PITCH_RATE + AttitudeTarget.IGNORE_ROLL_RATE)
        msg.body_rate.z = req.yaw_rate
        return attitude_pub, msg

    elif isinstance(req, srv.SetRatesYawRequest):
        ps.header.frame_id = req.frame_id or LOCAL_FRAME
        ps.pose.orientation = orientation_from_euler(0, 0, req.yaw)
        pose_local = tf_buffer.transform(ps, LOCAL_FRAME, TRANSFORM_TIMEOUT)
        msg = AttitudeTarget(orientation=pose_local.pose.orientation,
                             thrust=req.thrust,
                             type_mask=AttitudeTarget.IGNORE_YAW_RATE,
                             body_rate=Vector3(req.roll_rate, req.pitch_rate, 0))
        return attitude_pub, msg

    elif isinstance(req, srv.SetRatesRequest):
        msg = AttitudeTarget(thrust=req.thrust,
                             type_mask=AttitudeTarget.IGNORE_ATTITUDE,
                             body_rate=Vector3(req.roll_rate, req.pitch_rate, req.yaw_rate))
        return attitude_pub, msg


current_pub = None
current_msg = None
current_req = None
current_nav_start = None
current_nav_finish = None
current_nav_start_stamp = None
handle_lock = Lock()


def handle(req):
    global current_pub, current_msg, current_req

    if not state or not state.connected:
        rospy.logwarn('No connection to the FCU')
        return {'message': 'No connection to the FCU'}

    if isinstance(req, (srv.NavigateRequest, srv.NavigateGlobalRequest)) and req.speed <= 0:
        rospy.logwarn('Navigate speed must be greater than zero, %s passed')
        return {'message': 'Navigate speed must be greater than zero, %s passed' % req.speed}

    try:
        with handle_lock:
                stamp = rospy.get_rostime()
                current_req = req
                current_pub, current_msg = get_publisher_and_message(req, stamp, False)
                rospy.loginfo('Topic: %s, message: %s', current_pub.name, current_msg)

                current_msg.header.stamp = stamp
                current_pub.publish(current_msg)

        if req.auto_arm:
            offboard_and_arm()
        else:
            if state.mode != 'OFFBOARD':
                return {'message': 'Copter is not in OFFBOARD mode, use auto_arm?'}
            if not state.armed:
                return {'message': 'Copter is not armed, use auto_arm?'}

        return {'success': True}

    except Exception as e:
        rospy.logerr(str(e))
        return {'success': False, 'message': str(e)}


def land(req):
    rospy.loginfo('request %s mode' % LAND_MODE)
    res = set_mode(custom_mode=LAND_MODE)
    if not res.mode_sent:
        return {'message': 'Cannot send %s mode request' % LAND_MODE}

    start = rospy.get_rostime()
    while True:
        if state.mode == LAND_MODE:
            return {'success': True}
        if rospy.get_rostime() - start > LAND_TIMEOUT:
            return {'message': '%s mode request timed out' % LAND_MODE}
        rospy.sleep(0.1)


def release(req):
    global current_pub
    current_pub = None
    rospy.loginfo('simple_offboard: release')
    return {'success': True}


rospy.Service('navigate', srv.Navigate, handle)
rospy.Service('navigate_global', srv.NavigateGlobal, handle)
rospy.Service('set_position', srv.SetPosition, handle)
rospy.Service('set_position/yaw_rate', srv.SetPositionYawRate, handle)
rospy.Service('set_position_global', srv.SetPositionGlobal, handle)
rospy.Service('set_position_global/yaw_rate', srv.SetPositionGlobalYawRate, handle)
rospy.Service('set_velocity', srv.SetVelocity, handle)
rospy.Service('set_velocity/yaw_rate', srv.SetVelocityYawRate, handle)
rospy.Service('set_attitude', srv.SetAttitude, handle)
rospy.Service('set_attitude/yaw_rate', srv.SetAttitudeYawRate, handle)
rospy.Service('set_rates', srv.SetRates, handle)
rospy.Service('set_rates/yaw', srv.SetRatesYaw, handle)
rospy.Service('land', Trigger, land)
rospy.Service('release', Trigger, release)


def get_telemetry(req):
    res = {
        'frame_id': req.frame_id or LOCAL_FRAME,
        'x': float('nan'),
        'y': float('nan'),
        'z': float('nan'),
        'lat': float('nan'),
        'lon': float('nan'),
        'vx': float('nan'),
        'vy': float('nan'),
        'vz': float('nan'),
        'pitch': float('nan'),
        'roll': float('nan'),
        'yaw': float('nan'),
        'pitch_rate': float('nan'),
        'roll_rate': float('nan'),
        'yaw_rate': float('nan'),
        'voltage': float('nan'),
        'cell_voltage': float('nan')
    }
    frame_id = req.frame_id or LOCAL_FRAME
    stamp = rospy.get_rostime()

    if pose:
        p = tf_buffer.transform(pose, frame_id, TRANSFORM_TIMEOUT)
        res['x'] = p.pose.position.x
        res['y'] = p.pose.position.y
        res['z'] = p.pose.position.z
        # Get yaw in the request's frame_in
        _, _, res['yaw'] = euler_from_orientation(p.pose.orientation)
        # Calculate pitch and roll as angles between the pose and fcu_horiz
        attitude_pose = tf_buffer.transform(pose, 'fcu_horiz', TRANSFORM_TIMEOUT)
        res['roll'], res['pitch'], _ = euler_from_orientation(attitude_pose.pose.orientation)

    if velocity:
        v = Vector3Stamped()
        v.header.stamp = velocity.header.stamp
        v.header.frame_id = velocity.header.frame_id
        v.vector = velocity.twist.linear
        linear = tf_buffer.transform(v, frame_id, TRANSFORM_TIMEOUT)
        res['vx'] = linear.vector.x
        res['vy'] = linear.vector.y
        res['vz'] = linear.vector.z
        # TODO pitch_rate, roll_rate, yaw_rate

    if global_position and stamp - global_position.header.stamp < rospy.Duration(5):
        res['lat'] = global_position.latitude
        res['lon'] = global_position.longitude

    if state:
        res['connected'] = state.connected
        res['armed'] = state.armed
        res['mode'] = state.mode

    if battery:
        res['voltage'] = battery.voltage
        try:
            res['cell_voltage'] = battery.cell_voltage[0]
        except:
            pass

    return res


rospy.Service('get_telemetry', srv.GetTelemetry, get_telemetry)


rospy.loginfo('simple_offboard inited')


def start_loop():
    global current_pub, current_msg, current_req
    r = rospy.Rate(SETPOINT_RATE)

    while not rospy.is_shutdown():
        with handle_lock:
            if current_pub is not None:
                try:
                    stamp = rospy.get_rostime()

                    if getattr(current_req, 'update_frame', False) or \
                            isinstance(current_req, (srv.NavigateRequest, srv.NavigateGlobalRequest)):
                        current_pub, current_msg = get_publisher_and_message(current_req, stamp, True,
                                                                             getattr(current_req, 'update_frame', False))

                    current_msg.header.stamp = stamp
                    current_pub.publish(current_msg)

                    # For monitoring
                    if isinstance(current_msg, PositionTarget):
                        p = PoseStamped()
                        p.header.frame_id = LOCAL_FRAME
                        p.header.stamp = stamp
                        p.pose.position = current_msg.position
                        p.pose.orientation = orientation_from_euler(0, 0, current_msg.yaw + math.pi / 2)
                        target_pub.publish(p)

                except Exception as e:
                    rospy.logwarn_throttle(10, str(e))

        r.sleep()


start_loop()
