import numpy as np

# "Practical conversion from torsion space to Cartesian space for in silico protein synthesis"
#     https://pubmed.ncbi.nlm.nih.gov/15898109/
# Natural Extension Reference Frame algorithm
def nerf(a, b, c, bond_length, bond_angle_deg, torsion_angle_deg):
    """
    Parameters:
    - a, b, c: (3,) numpy arrays of xyz coordinates of the previous atoms.
    - bond_length: distance between c and d (Angstroms).
    - bond_angle_deg: angle b-c-d (Degrees).
    - torsion_angle_deg: dihedral angle a-b-c-d (Degrees).
    
    Returns:
    - d: (3,) numpy array of coordinates for the new atom.
    """
    # convert to radians
    bond_angle_rad = np.radians(bond_angle_deg)
    torsion_angle_rad = np.radians(torsion_angle_deg)
    
    # firstly calculate the position of D in the local reference frame
    # the bond b-c is the local x axis
   

    bond_angle_complement = np.pi - bond_angle_rad
    cd_local = np.array([
        bond_length * np.cos(bond_angle_complement),
        bond_length * np.sin(bond_angle_complement) * np.cos(torsion_angle_rad),
        bond_length * np.sin(bond_angle_complement) * np.sin(torsion_angle_rad)
    ])
    
    # next few steps are converting from local frame to global frame 
    bc = c - b
    bc_u = bc / np.linalg.norm(bc)
    ab = b - a
    
    # normal to the plane a-b-c is the local z axis
    n = np.cross(ab, bc_u)
    n_u = n / np.linalg.norm(n)
    
    # the local y axis is perpendicular to both the bond bc and the normal
    m_u = np.cross(n_u, bc_u)
    
    # rotation matrix columns: [x_axis, y_axis, z_axis] (3,3)
    rotation_matrix = np.column_stack((bc_u, m_u, n_u))
    
    # Global_Pos = Origin(C) + (Rotation * Local_Pos)
    d_global = c + np.dot(rotation_matrix, cd_local)
    
    return d_global
