"""Calculations described in SIDD Volume 1 section 7."""

import dataclasses

import numpy as np
import numpy.typing as npt

import sarkit.wgs84


@dataclasses.dataclass(kw_only=True)
class Angles:
    """Angles computed in SIDD 3.0.0 section 7."""

    # Geometry
    Azimuth: float  # 7.5.1 - Azimuth Angle
    Slope: float  # 7.5.2 - Slope Angle
    DopplerCone: float  # 7.5.3 - Doppler Cone Angle
    Squint: float  # 7.5.4 - Squint
    Graze: float  # 7.5.5 - Grazing Angle
    Tilt: float  # 7.5.6 - Tilt Angle
    # Phenomenology
    Shadow: float  # 7.6.1 - Shadow
    ShadowMagnitude: float
    Layover: float  # 7.6.2 - Layover
    LayoverMagnitude: float
    MultiPath: float  # 7.6.5 - Multi-Path
    GroundTrack: float  # 7.6.6 - Ground Track (Image Track) Angle
    # Product
    North: float  # 7.6.3 - North Direction


def compute_angles(
    scene_center_ecef: npt.ArrayLike,
    antenna_position_ecef: npt.ArrayLike,
    antenna_velocity_ecef: npt.ArrayLike,
    increasing_row_ecef: npt.ArrayLike,
    increasing_col_ecef: npt.ArrayLike,
) -> Angles:
    """Compute the angles defined in SIDD 3.0.0 section 7

    Parameters
    ----------
    scene_center_ecef : array_like
        Scene Center Point in ECEF coordinates.  (:math:`P_o`)
    antenna_position_ecef : array_like
        Antenna Position in ECEF coordinates.  (:math:`P_a`)
    antenna_velocity_ecef : array_like
        Antenna Velocity in ECEF coordinates.  (:math:`V_a`)
    increasing_row_ecef : array_like
        Unit Vector in increasing rows, in ECEF coordinates.  (:math:`\\hat{r}`)
    increasing_col_ecef : array_like
        Unit Vector in increasing columns, in ECEF coordinates.  (:math:`\\hat{c}`)

    Returns
    -------
    `Angles`
        Computed angle values
    """
    P_o = np.asarray(scene_center_ecef)  # noqa: N806
    P_a = np.asarray(antenna_position_ecef)  # noqa: N806
    V_a = np.asarray(antenna_velocity_ecef)  # noqa: N806
    r_hat = np.asarray(increasing_row_ecef)
    c_hat = np.asarray(increasing_col_ecef)

    # Table 7-1 defines lambda as Latitude and phi as Longitude, but the angle calculations imply the opposite.
    phi, lam = np.deg2rad(sarkit.wgs84.cartesian_to_geodetic(P_o)[:2])

    def _unit(vec):
        return vec / np.linalg.norm(vec, axis=-1)

    # 7.2 - Slant Plane Definition
    X_s_hat = _unit(P_a - P_o)  # noqa: N806
    n_hat = _unit(np.cross(X_s_hat, V_a))
    Z_s_hat = np.sign(np.dot(P_o, n_hat)) * n_hat  # noqa: N806
    Y_s_hat = np.cross(Z_s_hat, X_s_hat)  # noqa: N806

    # 7.2.1 - Image Plane Definition
    z_hat = np.cross(r_hat, c_hat)

    # 7.6.3 - North Direction
    N_hat = np.array(  # noqa: N806
        [-np.sin(phi) * np.cos(lam), -np.sin(phi) * np.sin(lam), np.cos(phi)]
    )
    N_prime = N_hat - np.dot(N_hat, z_hat) / np.dot(Z_s_hat, z_hat) * Z_s_hat  # noqa: N806
    theta_N = np.arctan2(np.dot(r_hat, N_prime), np.dot(c_hat, N_prime))  # noqa: N806

    # 7.6.4 - Up Direction
    Z_g_hat = U = np.array(  # noqa: N806
        [np.cos(phi) * np.cos(lam), np.cos(phi) * np.sin(lam), np.sin(phi)]
    )

    # 7.5.1 - Azimuth Angle
    E_hat = np.cross(N_hat, U)  # noqa: N806
    theta_a = np.arctan2(np.dot(E_hat, X_s_hat), np.dot(N_hat, X_s_hat))

    # 7.5.2 - Slope Angle
    psi_0 = np.arccos(np.dot(Z_s_hat, Z_g_hat))

    # 7.5.3 - Doppler Cone Angle
    V_a_hat = _unit(V_a)  # not defined in document  # noqa: N806
    Phi_s = np.arccos(-np.dot(X_s_hat, V_a_hat))  # noqa: N806

    # 7.5.4 - Squint
    Z_p_hat = _unit(P_a)  # noqa: N806
    X_s_prime = X_s_hat - np.dot(X_s_hat, Z_p_hat) * Z_p_hat  # noqa: N806
    V_a_prime = V_a - np.dot(V_a, Z_p_hat) * Z_p_hat  # noqa: N806
    X_s_prime_hat = _unit(X_s_prime)  # not defined in document  # noqa: N806
    V_a_prime_hat = _unit(V_a_prime)  # not defined in document  # noqa: N806
    Phi_g = np.arccos(-np.dot(X_s_prime_hat, V_a_prime_hat))  # noqa: N806

    # look direction seems to be omitted from the calculation in the spec but is referenced in the table
    uleft = _unit(np.cross(P_a, V_a))
    look = +1 if np.dot(uleft, X_s_hat) < 0 else -1
    Phi_g *= look  # noqa: N806

    # 7.5.5 - Grazing Angle
    Psi = np.arcsin(np.dot(X_s_hat, Z_g_hat))  # noqa: N806

    # 7.5.6 - Tilt Angle
    eta = np.arctan(np.dot(Z_g_hat, Y_s_hat) / np.dot(Z_g_hat, Z_s_hat))

    # 7.6 Phenomenology
    # 7.6.1 - Shadow
    S = Z_g_hat - X_s_hat / np.dot(X_s_hat, Z_g_hat)  # noqa: N806
    S_prime = S - np.dot(S, z_hat) / np.dot(Z_s_hat, z_hat) * Z_s_hat  # noqa: N806
    theta_S = np.arctan2(np.dot(r_hat, S_prime), np.dot(c_hat, S_prime))  # noqa: N806
    magnitude_S = np.sqrt(np.dot(S_prime, S_prime))  # noqa: N806

    # 7.6.2 - Layover
    L = z_hat - Z_s_hat / np.dot(Z_s_hat, z_hat)  # noqa: N806
    theta_L = np.arctan2(np.dot(r_hat, L), np.dot(c_hat, L))  # noqa: N806
    magnitude_L = np.sqrt(np.dot(L, L))  # noqa: N806

    # 7.6.5 - Multi-Path
    M = X_s_hat - np.dot(X_s_hat, z_hat) / np.dot(Z_s_hat, z_hat) * Z_s_hat  # noqa: N806
    theta_M = np.arctan2(np.dot(r_hat, M), np.dot(c_hat, M))  # noqa: N806

    # 7.6.6 - Ground Track (Image Track) Angle
    T = V_a - np.dot(V_a, z_hat) * z_hat  # noqa: N806
    theta_T = np.arctan2(np.dot(r_hat, T), np.dot(c_hat, T))  # noqa: N806

    return Angles(
        Azimuth=np.rad2deg(theta_a) % 360,
        Slope=np.rad2deg(psi_0),
        DopplerCone=np.rad2deg(Phi_s),
        Squint=np.rad2deg(Phi_g),
        Graze=np.rad2deg(Psi),
        Tilt=np.rad2deg(eta),
        Shadow=np.rad2deg(theta_S) % 360,
        ShadowMagnitude=magnitude_S,
        Layover=np.rad2deg(theta_L) % 360,
        LayoverMagnitude=magnitude_L,
        MultiPath=np.rad2deg(theta_M) % 360,
        GroundTrack=np.rad2deg(theta_T) % 360,
        North=np.rad2deg(theta_N) % 360,
    )
