from random import shuffle
from typing import List, Tuple
import numpy as np
from functools import lru_cache
from numpy.typing import NDArray


@lru_cache
def generate_rotation_matrices(count: int) -> List[NDArray[np.float64]]:
    axes = fibonacci_sphere(count)
    shuffle(axes)
    angles = np.linspace(0, 2 * np.pi, count, endpoint=False)
    matrices = [_rotation_matrix(axis, angle) for axis, angle in zip(axes, angles)]
    for matrix in matrices:
        _check_rotation_matrix(matrix)
    return matrices


def fibonacci_sphere(samples: int) -> List[Tuple[float, float, float]]:
    points = []
    phi = np.pi * (3.0 - np.sqrt(5.0))  # Golden angle in radians
    for i in range(samples):
        y = 1 - (i / float(samples - 1)) * 2  # y goes from 1 to -1
        radius = np.sqrt(1 - y * y)  # radius at y

        theta = phi * i  # golden angle increment

        x = np.cos(theta) * radius
        z = np.sin(theta) * radius

        points.append([x, y, z])
    return points


def _rotation_matrix(
    axis: Tuple[float, float, float], theta: float
) -> NDArray[np.float64]:
    axis = np.asarray(axis)
    axis = axis / np.sqrt(np.dot(axis, axis))
    a = np.cos(theta / 2.0)
    b, c, d = -axis * np.sin(theta / 2.0)

    aa, bb, cc, dd = a * a, b * b, c * c, d * d
    bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
    return np.array(
        [
            [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
            [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
            [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],
        ]
    )


def _check_rotation_matrix(R: NDArray[np.float64]):
    # Check if the matrix is square
    if R.shape != (3, 3):
        raise ValueError("Matrix must be 3x3.")

    # Check orthogonality: R.T * R should be close to the identity matrix
    I = np.eye(3)
    if not np.allclose(np.dot(R.T, R), I):
        raise ValueError("allclose")

    # Check determinant: Should be +1
    if not np.isclose(np.linalg.det(R), 1.0):
        raise ValueError(f"det {np.linalg.det(R)}")
