import collections

from math import cos
from math import radians
from math import sin

import numpy as np
from numpy import matmul


Vector = collections.namedtuple('Vector', ['x', 'y', 'z'])


class RotoTranslation:
    def __init__(self, rotation: Vector, translation: Vector, angle_unit: str, notation: str='XYZ'):
        self.rotation    = rotation
        self.translation = translation

        self.angle_unit = angle_unit
        if self.angle_unit == 'degrees':
            self.rotation = Vector(*[radians(alpha) for alpha in rotation])

        self.R_x = None
        self.R_y = None
        self.R_z = None
        self.T   = None

        self.matrix = None
        self.notation = notation

        self._update_matrix()

    def _update_matrix(self):
        # Convention: counter-clockwise rotation
        self.R_x = np.array([[1.0,              0.0,               0.0,              0.0],
                             [0.0,              cos(self.rotation.x),  -sin(self.rotation.x),  0.0],
                             [0.0,              sin(self.rotation.x),   cos(self.rotation.x),  0.0],
                             [0.0,              0.0,               0.0,              1.0]])

        self.R_y = np.array([[cos(self.rotation.y),  0.0,               sin(self.rotation.y),  0.0],
                             [0.0,              1.0,               0.0,              0.0],
                             [-sin(self.rotation.y), 0.0,               cos(self.rotation.y),  0.0],
                             [0.0,              0.0,               0.0,              1.0]])

        self.R_z = np.array([[cos(self.rotation.z),  -sin(self.rotation.z),   0.0,              0.0],
                             [sin(self.rotation.z),   cos(self.rotation.z),   0.0,              0.0],
                             [0.0,               0.0,               1.0,              0.0],
                             [0.0,               0.0,               0.0,              1.0]])

        self.T   = np.array([[1.0,               0.0,               0.0,              self.translation.x],
                             [0.0,               1.0,               0.0,              self.translation.y],
                             [0.0,               0.0,               1.0,              self.translation.z],
                             [0.0,               0.0,               0.0,              1.0]])

        if self.notation == 'XYZ':
            self.R = matmul(self.R_z, matmul(self.R_y, self.R_x))
        if self.notation == 'YXZ':
            self.R = matmul(self.R_z, matmul(self.R_x, self.R_y))
        if self.notation == 'YZX':
            self.R = matmul(self.R_x, matmul(self.R_z, self.R_y))
        if self.notation == 'XZY':
            self.R = matmul(self.R_y, matmul(self.R_z, self.R_x))
        if self.notation == 'ZYX':
            self.R = matmul(self.R_x, matmul(self.R_y, self.R_z))
        if self.notation == 'ZXY':
            self.R = matmul(self.R_y, matmul(self.R_x, self.R_z))

        self.matrix = matmul(self.T, self.R)

    def __str__(self):
        return RotoTranslation.pretty_string(self.matrix)

    @staticmethod
    def pretty_string(matrix, border=False, border_len=40):
        pretty_str = '\n'.join([''.join(['{:9.3f}'.format(item) for item in row]) for row in matrix])
        if border:
            pretty_str = border_len * '*' + '\n' + pretty_str + '\n' + border_len * '*'
        return pretty_str

    @property
    def alpha_x(self):
        alpha_x = self.rotation.x
        if self.angle_unit == 'degrees':
            alpha_x = np.degrees(alpha_x)
        return alpha_x

    @property
    def alpha_y(self):
        alpha_y = self.rotation.y
        if self.angle_unit == 'degrees':
            alpha_y = np.degrees(alpha_y)
        return alpha_y

    @property
    def alpha_z(self):
        alpha_z = self.rotation.z
        if self.angle_unit == 'degrees':
            alpha_z = np.degrees(alpha_z)
        return alpha_z

    @property
    def t_x(self):
        return self.translation.x

    @property
    def t_y(self):
        return self.translation.y

    @property
    def t_z(self):
        return self.translation.z

    @alpha_x.setter
    def alpha_x(self, value):
        if self.angle_unit == 'degrees':
            value = np.radians(value)

        self.rotation = Vector(value, self.rotation.y, self.rotation.z)
        self._update_matrix()

    @alpha_y.setter
    def alpha_y(self, value):
        if self.angle_unit == 'degrees':
            value = np.radians(value)

        self.rotation = Vector(self.rotation.x, value, self.rotation.z)
        self._update_matrix()

    @alpha_z.setter
    def alpha_z(self, value):
        if self.angle_unit == 'degrees':
            value = np.radians(value)

        self.rotation = Vector(self.rotation.x, self.rotation.y, value)
        self._update_matrix()

    @t_x.setter
    def t_x(self, value):
        self.translation = Vector(value, self.translation.y, self.translation.z)
        self._update_matrix()

    @t_y.setter
    def t_y(self, value):
        self.translation = Vector(self.translation.x, value, self.translation.z)
        self._update_matrix()

    @t_z.setter
    def t_z(self, value):
        self.translation = Vector(self.translation.x, self.translation.y, value)
        self._update_matrix()