from typing import Sequence
import astropy.units as u
import numpy as np
from astropy.units import Quantity
from ...containers import MuonRingContainer
__all__ = [
    "mean_squared_error",
    "intensity_ratio_inside_ring",
    "ring_completeness",
    "ring_containment",
    "ring_intensity_parameters",
    "radial_light_distribution",
]
[docs]
def mean_squared_error(
    pixel_fov_lon: Quantity,
    pixel_fov_lat: Quantity,
    weights: Quantity | np.ndarray | Sequence[float],
    ring: MuonRingContainer,
) -> float:
    """
    Calculate the weighted mean squared error for a circle.
    Parameters
    ----------
    pixel_fov_lon : Quantity
        Longitudes (x-coordinates) of the camera pixels in the TelescopeFrame.
    pixel_fov_lat : Quantity
        Latitudes (y-coordinates) of the camera pixels in the TelescopeFrame.
    weights : Quantity | np.ndarray | Sequence[float]
        Weights for the camera pixels, usually the photoelectron charges.
    ring : MuonRingContainer
        Container with the fitted ring parameters, including center coordinates and radius.
    Returns
    -------
    float
        The weighted mean squared error of the pixels around the fitted ring.
    Notes
    -----
    This function calculates the weighted mean squared error of the pixels around
    the fitted ring by determining the radial distance of each pixel from the ring
    center and comparing it to the ring radius. The mean squared error is weighted
    by the pixel intensities.
    """
    r = np.hypot(
        ring.center_fov_lon - pixel_fov_lon, ring.center_fov_lat - pixel_fov_lat
    )
    return np.average((r - ring.radius) ** 2, weights=weights) 
[docs]
def intensity_ratio_inside_ring(
    pixel_fov_lon: Quantity,
    pixel_fov_lat: Quantity,
    weights: Quantity | np.ndarray | Sequence[float],
    ring: MuonRingContainer,
    width: Quantity,
) -> float:
    """
    Calculate the ratio of the photons inside a given ring with
    coordinates (center_fov_lon, center_fov_lat), radius and width.
    The ring is assumed to be in [radius - 0.5 * width, radius + 0.5 * width]
    Parameters
    ----------
    pixel_fov_lon : Quantity
        Longitudes (x-coordinates) of the camera pixels in the TelescopeFrame.
    pixel_fov_lat : Quantity
        Latitudes (y-coordinates) of the camera pixels in the TelescopeFrame.
    weights : Quantity | np.ndarray | Sequence[float]
        Weights for the camera pixels, usually the photoelectron charges.
    ring : MuonRingContainer
        Container with the fitted ring parameters, including center coordinates and radius.
    width : Quantity
        Width of the ring.
    Returns
    -------
    float
        The ratio of the photons inside the ring to the total photons.
    Notes
    -----
    This function calculates the ratio of the photons inside a given ring by
    determining the pixels that fall within the specified ring width and summing
    their weights. The ratio is the sum of the weights inside the ring divided by
    the total sum of the weights.
    """
    pixel_r = np.hypot(
        ring.center_fov_lon - pixel_fov_lon, ring.center_fov_lat - pixel_fov_lat
    )
    mask = np.logical_and(
        pixel_r >= ring.radius - 0.5 * width, pixel_r <= ring.radius + 0.5 * width
    )
    inside = weights[mask].sum()
    total = weights.sum()
    return inside / total 
[docs]
def ring_completeness(
    pixel_fov_lon: Quantity,
    pixel_fov_lat: Quantity,
    weights: Quantity | np.ndarray | Sequence[float],
    ring: MuonRingContainer,
    threshold: float = 30,
    bins: int = 30,
) -> float:
    """
    Estimate how complete a muon ring is by binning the light distribution along the ring
    and applying a threshold to the bin content.
    Parameters
    ----------
    pixel_fov_lon : Quantity
        Longitudes (x-coordinates) of the camera pixels in the TelescopeFrame.
    pixel_fov_lat : Quantity
        Latitudes (y-coordinates) of the camera pixels in the TelescopeFrame.
    weights : array-like
        Weights for the camera pixels, usually the photoelectron charges.
    ring : MuonRingContainer
        Container with the fitted ring parameters, including center coordinates and radius.
    threshold : float, optional
        Number of photoelectrons a bin must contain to be counted. Default is 30.
    bins : int, optional
        Number of bins to use for the histogram. Default is 30.
    Returns
    -------
    float
        The ratio of bins above the threshold, representing the completeness of the ring.
    Notes
    -----
    This function calculates the completeness of the muon ring by dividing the ring into
    segments and counting the number of segments that have a light intensity above a given
    threshold. The completeness is the ratio of the number of segments above the threshold
    to the total number of segments.
    """
    if hasattr(weights, "unit"):
        weights = weights.to_value(u.dimensionless_unscaled)
    angle = np.arctan2(
        (pixel_fov_lat - ring.center_fov_lat).to_value(u.rad),
        (pixel_fov_lon - ring.center_fov_lon).to_value(u.rad),
    )
    hist, _ = np.histogram(
        angle,
        bins=bins,
        range=[-np.pi, np.pi],
        weights=weights,
    )
    bins_above_threshold = hist > threshold
    return np.sum(bins_above_threshold) / bins 
[docs]
def ring_containment(ring: MuonRingContainer, camera_radius: Quantity) -> float:
    """
    Estimate the angular containment of a muon ring inside the camera's field of view.
    This function calculates the fraction of the muon ring that is contained within
    the camera's field of view. It uses geometric properties to determine the intersection
    of the ring with the circular boundary of the camera.
    Parameters
    ----------
    ring : MuonRingContainer
        Container with the fitted ring parameters, including center coordinates and radius.
    camera_radius : `astropy.units.Quantity`
        Radius of the camera's field of view (in degrees).
    Returns
    -------
    float
        The fraction of the ring that is inside the camera's field of view, ranging from 0.0 to 1.0.
    Notes
    -----
    The calculation is based on the geometric intersection of two circles:
    the muon ring and the camera's field of view. The method handles cases where:
    - The ring is fully contained within the camera.
    - The ring is partially contained within the camera.
    - The ring is completely outside the camera.
    References
    ----------
    See https://stackoverflow.com/questions/3349125/circle-circle-intersection-points
    for the geometric approach to circle-circle intersection.
    """
    # one circle fully contained in the other
    if ring.center_distance <= np.abs(camera_radius - ring.radius):
        return 1.0
    # no intersection
    if ring.center_distance > (ring.radius + camera_radius):
        return 0.0
    a = (ring.radius**2 - camera_radius**2 + ring.center_distance**2) / (
        2 * ring.center_distance
    )
    return np.arccos((a / ring.radius).to_value(u.dimensionless_unscaled)) / np.pi 
def ring_intensity_parameters(
    ring: MuonRingContainer,
    pixel_fov_lon: Quantity,
    pixel_fov_lat: Quantity,
    ring_integration_width: float,
    outer_ring_width: float,
    image: np.ndarray,
    image_mask: np.ndarray,
) -> tuple[float, float, int, float]:
    """
    Calculate the parameters related to the size of the ring image.
    Parameters
    ----------
    ring : MuonRingContainer
        Container with the fitted ring parameters, including center coordinates and radius.
    pixel_fov_lon : Quantity
        Longitudes (x-coordinates) of the camera pixels in the TelescopeFrame.
    pixel_fov_lat : Quantity
        Latitudes (y-coordinates) of the camera pixels in the TelescopeFrame.
    ring_integration_width : float
        Width of the ring in fractions of ring radius.
    outer_ring_width : float
        Width of the outer ring in fractions of ring radius.
    image : np.ndarray
        Amplitude of image pixels.
    image_mask : np.ndarray
        Mask of the camera pixels after cleaning.
    Returns
    -------
    ring_intensity : float
        Sum of the p.e. inside the integration area of the ring.
    intensity_outside_ring : float
        Sum of the p.e. outside the ring integration area that passed the cleaning.
    n_pixels_in_ring : int
        Number of pixels inside the ring integration area that passed the cleaning.
    mean_intensity_outside_ring : float
        Mean intensity of the pixels outside the integration area of the ring,
        and restricted by the outer ring width, i.e. in the strip between
        ring integration width and outer ring width.
    """
    dist = np.hypot(
        pixel_fov_lon - ring.center_fov_lon, pixel_fov_lat - ring.center_fov_lat
    )
    dist_mask = np.abs(dist - ring.radius) < (ring.radius * ring_integration_width)
    pix_ring = image * dist_mask
    pix_outside_ring = image * ~dist_mask
    dist_mask_2 = np.logical_and(
        ~dist_mask,
        np.abs(dist - ring.radius)
        < ring.radius * (ring_integration_width + outer_ring_width),
    )
    pix_ring_2 = image[dist_mask_2]
    ring_intensity = np.sum(pix_ring)
    intensity_outside_ring = np.sum(pix_outside_ring * image_mask)
    n_pixels_in_ring = np.sum(dist_mask & image_mask)
    mean_intensity_outside_ring = np.sum(pix_ring_2) / len(pix_ring_2)
    return (
        ring_intensity,
        intensity_outside_ring,
        n_pixels_in_ring,
        mean_intensity_outside_ring,
    )
def radial_light_distribution(
    center_fov_lon, center_fov_lat, pixel_fov_lon, pixel_fov_lat, image
):
    """
    Calculate the radial distribution of the muon ring.
    Parameters
    ----------
    center_fov_lon : float
        Longitude of the ring center in the TelescopeFrame (in degrees).
    center_fov_lat : float
        Latitude of the ring center in the TelescopeFrame (in degrees).
    pixel_fov_lon : array-like
        Longitudes (x-coordinates) of the camera pixels in the TelescopeFrame (in degrees).
    pixel_fov_lat : array-like
        Latitudes (y-coordinates) of the camera pixels in the TelescopeFrame (in degrees).
    image : array-like
        Amplitudes of image pixels.
    Returns
    -------
    radial_std_dev : `astropy.units.Quantity`
        Standard deviation of the light distribution in degrees.
        Spread of pixel intensities around the mean radial distance from the ring center.
    skewness : `astropy.units.Quantity`
        Skewness of the radial light distribution (dimensionless).
        Measures the asymmetry of the distribution around the mean radius.
    excess_kurtosis : `astropy.units.Quantity`
        Excess kurtosis of the radial light distribution (dimensionless).
        Indicates the "tailedness" of the distribution compared to a normal distribution.
    Notes
    -----
    This function calculates the statistical properties of the radial distribution
    of light in an image with respect to the reconstructed muon ring center. It computes
    the standard deviation, skewness, and excess kurtosis of the radial distances of the pixels
    from the center of the ring, weighted by the pixel intensities.
    """
    if np.sum(image) == 0:
        return (
            np.nan * u.deg,
            np.nan,
            np.nan,
        )
    pixel_r = np.hypot(pixel_fov_lon - center_fov_lon, pixel_fov_lat - center_fov_lat)
    mean = np.average(pixel_r, weights=image)
    delta_r = pixel_r - mean
    radial_std_dev = np.sqrt(np.average(delta_r**2, weights=image))
    skewness = (np.average(delta_r**3, weights=image) / radial_std_dev**3).to_value()
    excess_kurtosis = (
        np.average(delta_r**4, weights=image) / radial_std_dev**4 - 3.0
    ).to_value()
    return (
        radial_std_dev,
        skewness,
        excess_kurtosis,
    )