"""
Python library to control an UR robot through its TCP/IP interface
DOC LINK
http://support.universal-robots.com/URRobot/RemoteAccess
"""


import math3d as m3d
import numpy as np

from urx.urrobot import URRobot

__author__ = "Olivier Roulet-Dubonnet"
__copyright__ = "Copyright 2011-2016, Sintef Raufoss Manufacturing"
__license__ = "LGPLv3"


class Robot(URRobot):

    """
    Generic Python interface to an industrial robot.
    Compared to the URRobot class, this class adds the possibilty to work directly with matrices
    and includes support for setting a reference coordinate system
    """

    def __init__(self, host, use_rt=False, urFirm=None):
        URRobot.__init__(self, host, use_rt, urFirm)
        self.csys = m3d.Transform()

    def _get_lin_dist(self, target):
        pose = URRobot.getl(self, wait=True)
        target = m3d.Transform(target)
        pose = m3d.Transform(pose)
        return pose.dist(target)

    def set_tcp(self, tcp):
        """
        set robot flange to tool tip transformation
        """
        if isinstance(tcp, m3d.Transform):
            tcp = tcp.pose_vector
        URRobot.set_tcp(self, tcp)

    def set_csys(self, transform):
        """
        Set reference coordinate system to use
        """
        self.csys = transform

    def set_orientation(self, orient, acc=0.01, vel=0.01, wait=True, threshold=None):
        """
        set tool orientation using a orientation matric from math3d
        or a orientation vector
        """
        if not isinstance(orient, m3d.Orientation):
            orient = m3d.Orientation(orient)
        trans = self.get_pose()
        trans.orient = orient
        self.set_pose(trans, acc, vel, wait=wait, threshold=threshold)

    def translate_tool(self, vect, acc=0.01, vel=0.01, wait=True, threshold=None):
        """
        move tool in tool coordinate, keeping orientation
        """
        t = m3d.Transform()
        if not isinstance(vect, m3d.Vector):
            vect = m3d.Vector(vect)
        t.pos += vect
        return self.add_pose_tool(t, acc, vel, wait=wait, threshold=threshold)

    def back(self, z=0.05, acc=0.01, vel=0.01):
        """
        move in z tool
        """
        self.translate_tool((0, 0, -z), acc=acc, vel=vel)

    def set_pos(self, vect, acc=0.01, vel=0.01, wait=True, threshold=None):
        """
        set tool to given pos, keeping constant orientation
        """
        if not isinstance(vect, m3d.Vector):
            vect = m3d.Vector(vect)
        trans = m3d.Transform(self.get_orientation(), m3d.Vector(vect))
        return self.set_pose(trans, acc, vel, wait=wait, threshold=threshold)

    def movec(self, pose_via, pose_to, acc=0.01, vel=0.01, wait=True, threshold=None):
        """
        Move Circular: Move to position (circular in tool-space)
        see UR documentation
        """
        pose_via = self.csys * m3d.Transform(pose_via)
        pose_to = self.csys * m3d.Transform(pose_to)
        pose = URRobot.movec(self, pose_via.pose_vector, pose_to.pose_vector, acc=acc, vel=vel, wait=wait, threshold=threshold)
        if pose is not None:
            return self.csys.inverse * m3d.Transform(pose)

    def set_pose(self, trans, acc=0.01, vel=0.01, wait=True, command="movel", threshold=None):
        """
        move tcp to point and orientation defined by a transformation
        UR robots have several move commands, by default movel is used but it can be changed
        using the command argument
        """
        self.logger.debug("Setting pose to %s", trans.pose_vector)
        t = self.csys * trans
        pose = URRobot.movex(self, command, t.pose_vector, acc=acc, vel=vel, wait=wait, threshold=threshold)
        if pose is not None:
            return self.csys.inverse * m3d.Transform(pose)

    def add_pose_base(self, trans, acc=0.01, vel=0.01, wait=True, command="movel", threshold=None):
        """
        Add transform expressed in base coordinate
        """
        pose = self.get_pose()
        return self.set_pose(trans * pose, acc, vel, wait=wait, command=command, threshold=threshold)

    def add_pose_tool(self, trans, acc=0.01, vel=0.01, wait=True, command="movel", threshold=None):
        """
        Add transform expressed in tool coordinate
        """
        pose = self.get_pose()
        return self.set_pose(pose * trans, acc, vel, wait=wait, command=command, threshold=threshold)

    def get_pose(self, wait=False, _log=True):
        """
        get current transform from base to to tcp
        """
        pose = URRobot.getl(self, wait, _log)
        trans = self.csys.inverse * m3d.Transform(pose)
        if _log:
            self.logger.debug("Returning pose to user: %s", trans.pose_vector)
        return trans

    def get_orientation(self, wait=False):
        """
        get tool orientation in base coordinate system
        """
        trans = self.get_pose(wait)
        return trans.orient

    def get_pos(self, wait=False):
        """
        get tool tip pos(x, y, z) in base coordinate system
        """
        trans = self.get_pose(wait)
        return trans.pos

    def speedl(self, velocities, acc, min_time):
        """
        move at given velocities until minimum min_time seconds
        """
        v = self.csys.orient * m3d.Vector(velocities[:3])
        w = self.csys.orient * m3d.Vector(velocities[3:])
        vels = np.concatenate((v.array, w.array))
        return self.speedx("speedl", vels, acc, min_time)

    def speedj(self, velocities, acc, min_time):
        """
        move at given joint velocities until minimum min_time seconds
        """
        return self.speedx("speedj", velocities, acc, min_time)

    def speedl_tool(self, velocities, acc, min_time):
        """
        move at given velocities in tool csys until minimum min_time seconds
        """
        pose = self.get_pose()
        v = pose.orient * m3d.Vector(velocities[:3])
        w = pose.orient * m3d.Vector(velocities[3:])
        self.speedl(np.concatenate((v.array, w.array)), acc, min_time)

    def movex(self, command, pose, acc=0.01, vel=0.01, wait=True, relative=False, threshold=None):
        """
        Send a move command to the robot. since UR robotene have several methods this one
        sends whatever is defined in 'command' string
        """
        t = m3d.Transform(pose)
        if relative:
            return self.add_pose_base(t, acc, vel, wait=wait, command=command, threshold=threshold)
        else:
            return self.set_pose(t, acc, vel, wait=wait, command=command, threshold=threshold)

    def movexs(self, command, pose_list, acc=0.01, vel=0.01, radius=0.01, wait=True, threshold=None):
        """
        Concatenate several movex commands and applies a blending radius
        pose_list is a list of pose.
        This method is usefull since any new command from python
        to robot make the robot stop
        """
        new_poses = []
        for pose in pose_list:
            t = self.csys * m3d.Transform(pose)
            pose = t.pose_vector
            new_poses.append(pose)
        return URRobot.movexs(self, command, new_poses, acc, vel, radius, wait=wait, threshold=threshold)

    def movel_tool(self, pose, acc=0.01, vel=0.01, wait=True, threshold=None):
        """
        move linear to given pose in tool coordinate
        """
        return self.movex_tool("movel", pose, acc=acc, vel=vel, wait=wait, threshold=threshold)

    def movex_tool(self, command, pose, acc=0.01, vel=0.01, wait=True, threshold=None):
        t = m3d.Transform(pose)
        self.add_pose_tool(t, acc, vel, wait=wait, command=command, threshold=threshold)

    def getl(self, wait=False, _log=True):
        """
        return current transformation from tcp to current csys
        """
        t = self.get_pose(wait, _log)
        return t.pose_vector.tolist()

    def set_gravity(self, vector):
        if isinstance(vector, m3d.Vector):
            vector = vector.list
        return URRobot.set_gravity(self, vector)

    def new_csys_from_xpy(self):
        """
        Restores and returns new coordinate system calculated from three points: X, Origin, Y

        based on math3d: Transform.new_from_xyp
        """
        # Set coord. sys. to 0
        self.csys = m3d.Transform()

        print("A new coordinate system will be defined from the next three points")
        print("Firs point is X, second Origin, third Y")
        print("Set it as a new reference by calling myrobot.set_csys(new_csys)")
        input("Move to first point and click Enter")
        # we do not use get_pose so we avoid rounding values
        pose = URRobot.getl(self)
        print("Introduced point defining X: {}".format(pose[:3]))
        px = m3d.Vector(pose[:3])
        input("Move to second point and click Enter")
        pose = URRobot.getl(self)
        print("Introduced point defining Origo: {}".format(pose[:3]))
        p0 = m3d.Vector(pose[:3])
        input("Move to third point and click Enter")
        pose = URRobot.getl(self)
        print("Introduced point defining Y: {}".format(pose[:3]))
        py = m3d.Vector(pose[:3])

        new_csys = m3d.Transform.new_from_xyp(px - p0, py - p0, p0)
        self.set_csys(new_csys)

        return new_csys

    @property
    def x(self):
        return self.get_pos().x

    @x.setter
    def x(self, val):
        p = self.get_pos()
        p.x = val
        self.set_pos(p)

    @property
    def y(self):
        return self.get_pos().y

    @y.setter
    def y(self, val):
        p = self.get_pos()
        p.y = val
        self.set_pos(p)

    @property
    def z(self):
        return self.get_pos().z

    @z.setter
    def z(self, val):
        p = self.get_pos()
        p.z = val
        self.set_pos(p)

    @property
    def rx(self):
        return 0

    @rx.setter
    def rx(self, val):
        p = self.get_pose()
        p.orient.rotate_xb(val)
        self.set_pose(p)

    @property
    def ry(self):
        return 0

    @ry.setter
    def ry(self, val):
        p = self.get_pose()
        p.orient.rotate_yb(val)
        self.set_pose(p)

    @property
    def rz(self):
        return 0

    @rz.setter
    def rz(self, val):
        p = self.get_pose()
        p.orient.rotate_zb(val)
        self.set_pose(p)

    @property
    def x_t(self):
        return 0

    @x_t.setter
    def x_t(self, val):
        t = m3d.Transform()
        t.pos.x += val
        self.add_pose_tool(t)

    @property
    def y_t(self):
        return 0

    @y_t.setter
    def y_t(self, val):
        t = m3d.Transform()
        t.pos.y += val
        self.add_pose_tool(t)

    @property
    def z_t(self):
        return 0

    @z_t.setter
    def z_t(self, val):
        t = m3d.Transform()
        t.pos.z += val
        self.add_pose_tool(t)

    @property
    def rx_t(self):
        return 0

    @rx_t.setter
    def rx_t(self, val):
        t = m3d.Transform()
        t.orient.rotate_xb(val)
        self.add_pose_tool(t)

    @property
    def ry_t(self):
        return 0

    @ry_t.setter
    def ry_t(self, val):
        t = m3d.Transform()
        t.orient.rotate_yb(val)
        self.add_pose_tool(t)

    @property
    def rz_t(self):
        return 0

    @rz_t.setter
    def rz_t(self, val):
        t = m3d.Transform()
        t.orient.rotate_zb(val)
        self.add_pose_tool(t)
