#! /usr/bin/env python
"""The script for calibrating the arm lengths and 'joystick' extents.

You will normally not call this script directly but rather with
`roslaunch teleoperation calibration.launch`
"""

import os
import json

import rospy
import tf
import actionlib
import numpy as np
from naoqi import ALProxy

from teleoperation.msg import RequestSpeechAction, RequestSpeechGoal


def calib_field(sentences, src_frames, targ_frames):
    """Retrieve a relative transform of some marker.

    Parameters
    ----------

    sentences : list of str
        What to say.
    src_frames : list of str
        The names of tf source frames (from where to transform)
    targ_frames : list of str
        The names of tf target frames (to where to transform)

    Returns
    -------
    list of transforms

    """
    ok = False
    t_and_q = []

    while not ok:
        for s in sentences:
            tts.say(s)
        tts.say('Then say start')
        client.send_goal(RequestSpeechGoal(['start']))
        client.wait_for_result()
        for src, targ in zip(src_frames, targ_frames):
            try:
                t_and_q.append(ll.lookupTransform(src, targ, rospy.Time(0)))
                ok = True
            except:
                ok = False
                tts.say('Step failed. Try again')
                break

    return t_and_q


def calib_center():
    """Retrieve the coordinates of the 'joystick' zero state."""
    trans, q = calib_field([
        'Stand in front of camera',
        'Far enough to see your arms'
    ], ['odom'], ['Aruco_0_frame'])[0]
    rot = tf.transformations.euler_from_quaternion(q)
    return trans, rot


def calib_arms():
    """Retrieve the hand positions relative to the chest."""
    ts_and_qs = calib_field(
        ['Now stretch your arms', 'Make sure the markers are detected'],
        ['odom'] * 3,
        ['Aruco_{}_frame'.format(i) for i in range(3)]
    )
    a0 = ts_and_qs[0][0]
    a1 = ts_and_qs[1][0]
    a2 = ts_and_qs[2][0]
    return (
        tuple(c1 - c0 for c0, c1 in zip(a0, a1)),
        tuple(c2 - c0 for c0, c2 in zip(a0, a2))
    )


def calib_shoulders():
    """Retrieve the shoulder positions relative to the chest."""
    ts_and_qs = calib_field(
        ['Now place the markers on the corresponding shoulders',
         'Make sure the markers are detected'],
        ['odom'] * 3,
        ['Aruco_{}_frame'.format(i) for i in range(3)]
    )
    a0 = ts_and_qs[0][0]
    a1 = ts_and_qs[1][0]
    a2 = ts_and_qs[2][0]
    return (
        tuple(c1 - c0 for c0, c1 in zip(a0, a1)),
        tuple(c2 - c0 for c0, c2 in zip(a0, a2))
    )


def calib_rotation():
    """Retrieve the limits of the z-axis of the 'joystick'."""
    rots = []
    for side in ('left', 'right'):
        _, q = calib_field(
            ['Now rotate to your {}'.format(side),
             'So that marker on the chest is still detected'],
            ['odom'], ['Aruco_0_frame']
        )[0]
        rots.append(tf.transformations.euler_from_quaternion(q))
    return tuple(rots)


def calib_extremes():
    """Retrieve the limits of the x and y axes of the 'joystick'."""
    transs = []
    for direction in ('forward', 'backward', 'to the left', 'to the right'):
         trans, _ = calib_field(
            ['Now return to the initial position',
             'Take a big step {}'.format(direction)],
            ['odom'],
            ['Aruco_0_frame']
         )[0]
         transs.append(trans)

    return tuple(transs)


def calibration():
    """Run full calibration and dump the results to the config file."""
    center = calib_center()
    larm, rarm = calib_arms()
    lsh, rsh = calib_shoulders()
    larm, rarm, lsh, rsh = larm[1:], rarm[1:], lsh[1:], rsh[1:]
    lr, rr = calib_rotation()
    fw, bk, lt, rt = calib_extremes()

    tts.say('Well done')

    calib = {
        'cr': [center[0][0], center[0][1], center[1][2]],
        'arm': (np.linalg.norm(np.array(larm) - np.array(lsh)) +
                np.linalg.norm(np.array(rarm) - np.array(rsh))) / 2,
        'lsh': [0.0] + list(lsh),
        'rsh': [0.0] + list(rsh),
        'rr': rr[2],
        'lr': lr[2],
        'fw': fw[0],
        'bk': bk[0],
        'lt': lt[1],
        'rt': rt[1],
    }

    here = os.path.dirname(os.path.realpath(__file__))
    with open('{}/../config/default.yaml'.format(here), 'w') as f:
        json.dump(calib, f, indent=4)


if __name__ == '__main__':

    rospy.init_node('calibrator', disable_signals=True)

    try:
        tts = ALProxy('ALTextToSpeech', os.environ['NAO_IP'], 9559)
        client = actionlib.SimpleActionClient('speech_server',
                                              RequestSpeechAction)
        client.wait_for_server()

        ll = tf.TransformListener()

        rospy.on_shutdown(lambda: client.cancel_goal())

        calibration()

    except KeyboardInterrupt:
        rospy.signal_shutdown('Interrupted')
