"""Reusable dynamics DSP utilities for AI MusiMuse.

This module provides shared helper functions for time-domain dynamic
analysis.  All dynamic analyzers must use these utilities instead of
implementing RMS or envelope logic independently.

The DSP package is the single source of truth for dynamic processing
throughout the project.
"""

from __future__ import annotations

import numpy as np


def compute_rms_envelope(
    samples: np.ndarray,
    window_size: int,
    hop_size: int,
) -> np.ndarray:
    """Compute the short-term RMS envelope of a 1-D signal.

    Uses a sliding window with the given size and hop.  Each frame
    contains the root-mean-square of the samples within the window.

    Args:
        samples: 1-D mono audio signal.
        window_size: Number of samples per RMS window.
        hop_size: Hop size between frames in samples.

    Returns:
        1-D array of RMS values, one per frame.
    """
    n_samples = len(samples)
    if n_samples < window_size:
        rms = float(np.sqrt(np.mean(samples**2))) if n_samples > 0 else 0.0
        return np.array([rms], dtype=np.float64)

    n_frames = 1 + (n_samples - window_size) // hop_size

    window_view = np.lib.stride_tricks.sliding_window_view(samples, window_size)
    window_view = window_view[::hop_size][:n_frames]

    rms_frames = np.sqrt(np.mean(window_view**2, axis=1))
    return rms_frames.astype(np.float64)


def compute_dynamic_range(rms_envelope: np.ndarray) -> float:
    """Compute dynamic range from an RMS envelope.

    The difference between the 95th percentile and the 10th percentile
    of the short-term RMS envelope, expressed in dB.

    Args:
        rms_envelope: 1-D array of RMS values.

    Returns:
        Dynamic range in dB (>= 0).
    """
    if len(rms_envelope) == 0:
        return 0.0

    positive = rms_envelope[rms_envelope > 0.0]
    if len(positive) < 2:
        return 0.0

    p95 = float(np.percentile(positive, 95))
    p10 = float(np.percentile(positive, 10))

    if p10 <= 0.0 or p95 <= 0.0:
        return 0.0

    db_95 = 20.0 * np.log10(p95)
    db_10 = 20.0 * np.log10(p10)
    return float(db_95 - db_10)


def compute_headroom(peak: float) -> float:
    """Compute headroom from peak amplitude.

    The distance between the peak level and 0 dBFS.

    Args:
        peak: Peak amplitude (linear, >= 0).

    Returns:
        Headroom in dB (>= 0).  Returns 0.0 for peak >= 1.0.
    """
    if peak >= 1.0:
        return 0.0
    if peak <= 0.0:
        return float("inf")
    return float(-20.0 * np.log10(peak))


def compute_clipping_ratio(
    samples: np.ndarray,
    threshold: float = 0.999,
) -> float:
    """Compute the clipping ratio of a signal.

    The fraction of samples whose absolute value is greater than or
    equal to the given threshold.

    Args:
        samples: 1-D audio signal.
        threshold: Clipping threshold (default 0.999).

    Returns:
        Clipping ratio in [0, 1].
    """
    if len(samples) == 0:
        return 0.0
    return float(np.mean(np.abs(samples) >= threshold))
