"""
======================================================================
Tesseract (Hypercube)
======================================================================
A tesseract is a four-dimensional cube. A tesseract can
be unfolded into eight cubes, Just as a cube can be unfolded into eight
squares.
"""

###############################################################################
# First, import some useful functions

import itertools

import numpy as np

import fury

###############################################################################
# Let's define some variables and their descriptions:
#
# Use `wireframe = True` to show wireframe like representation of the tesseract
# `wireframe = False` will render it with point actor on each vertex.

wireframe = False

# p_color: color of the point actor (default: (0, 0.5, 1, 1))
# e_color: color of the line actor (default: (1, 1, 1, 1))
# dtheta: change in `angle` on each iteration. It determines the "speed" of the
#        animation. Increase dtheta to increase speed of rotation, It may
#        result in less smoother rotation (default: 0.02)
# angle: defines the angle to be rotated to perform the animation, It changes
#        as we run the `callback` method later on. (initial value: 0)

p_color = np.array([0, 0.5, 1, 1])
e_color = np.array([1, 1, 1, 1])
dtheta = 0.02
angle = 0

###############################################################################
# Let's define vertices for our 4D cube, `verts4D` contains the coordinates of
# our 4D tesseract.

verts3D = np.array(
    [
        [1, 1, 1],
        [1, -1, 1],
        [-1, -1, 1],
        [-1, 1, 1],
        [-1, 1, -1],
        [1, 1, -1],
        [1, -1, -1],
        [-1, -1, -1],
    ]
)

# We can use primitive.box alternatively to get the cube's 3-D vertices.

u = np.insert(verts3D, 3, 1, axis=1)
v = np.insert(verts3D, 3, -1, axis=1)
verts4D = np.append(u, v, axis=0)

###############################################################################
# We define a `rotate4D` function that takes 4D matrix as parameter and rotates
# it in XY plane (Z axis) and ZW plane (an imaginary axis), projects it to the
# 3D plane so that we can render it in a scene.


def rotate4D(verts4D):
    cos = np.cos(angle)
    sin = np.sin(angle)
    rotation4d_xy = np.array(
        [[cos, -sin, 0, 0], [sin, cos, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
    )
    rotation4d_zw = np.array(
        [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, cos, -sin], [0, 0, sin, cos]]
    )
    distance = 2
    projected_marix = np.zeros((16, 3))
    for i, vert in enumerate(verts4D):
        rotated_3D = np.dot(rotation4d_xy, vert)
        rotated_3D = np.dot(rotation4d_zw, rotated_3D)
        w = 1 / (distance - rotated_3D[3])
        proj_mat4D = np.array([[w, 0, 0, 0], [0, w, 0, 0], [0, 0, w, 0]])

        projeced_mat3D = np.dot(proj_mat4D, rotated_3D)
        projected_marix[i] = projeced_mat3D  # vertices to be proj (3D)
    return projected_marix


###############################################################################
# Now, We have 4D points projected to 3D. Let's define a function to connect
# lines.


def connect_points(verts3D):
    lines = np.array([])
    len_vert = len(verts3D)

    for i in range(len_vert - 1):
        if i < 8:
            lines = np.append(lines, [verts3D[i], verts3D[i + 8]])
        if i == 7:
            pass
        else:
            lines = np.append(lines, [verts3D[i], verts3D[i + 1]])
        if i % 4 == 0:
            lines = np.append(lines, [verts3D[i], verts3D[i + 3]])

    for i in range(3):
        lines = np.append(lines, [verts3D[i], verts3D[i + 5]])
        lines = np.append(lines, [verts3D[i + 8], verts3D[i + 5 + 8]])

    return np.reshape(lines, (-1, 2, 3))


###############################################################################
# Creating a scene object and configuring the camera's position

scene = fury.window.Scene()
scene.set_camera(
    position=(0, 10, -1), focal_point=(0.0, 0.0, 0.0), view_up=(0.0, 0.0, 0.0)
)
showm = fury.window.ShowManager(scene=scene, size=(1920, 1080), order_transparent=True)


###############################################################################
# Creating vertices and points actors

verts3D = rotate4D(verts4D)
if not wireframe:
    points = fury.actor.point(verts3D, colors=p_color)
    point_verts = fury.utils.vertices_from_actor(points)
    no_vertices = len(point_verts) / 16
    initial_verts = point_verts.copy() - np.repeat(verts3D, no_vertices, axis=0)

    scene.add(points)

###############################################################################
# Connecting points with lines actor

lines = connect_points(verts3D)
edges = fury.actor.line(
    lines=lines, colors=e_color, lod=False, fake_tube=True, linewidth=4
)
lines_verts = fury.utils.vertices_from_actor(edges)
initial_lines = lines_verts.copy() - np.reshape(lines, (-1, 3))

scene.add(edges)

###############################################################################
# Initializing text box to display the name

tb = fury.ui.TextBlock2D(text="Tesseract", position=(900, 950), font_size=20)
showm.scene.add(tb)

###############################################################################
# Define a timer_callback in which we'll update the vertices of point and lines
# actor using `rotate4D`.

counter = itertools.count()
end = 200


def timer_callback(_obj, _event):
    global verts3D, angle
    cnt = next(counter)
    verts3D = rotate4D(verts4D)
    if not wireframe:
        point_verts[:] = initial_verts + np.repeat(verts3D, no_vertices, axis=0)
        fury.utils.update_actor(points)

    lines = connect_points(verts3D)
    lines_verts[:] = initial_lines + np.reshape(lines, (-1, 3))
    fury.utils.update_actor(edges)

    showm.render()
    angle += dtheta

    if cnt == end:
        showm.exit()


###############################################################################
# Run every 20 milliseconds


showm.add_timer_callback(True, 20, timer_callback)
showm.start()
fury.window.record(scene=showm.scene, size=(600, 600), out_path="viz_tesseract.png")
