"""
Signal-processing utilities: smoothing, peak/min finding, baseline,
baseline subtraction, sign inversion.
"""

import numpy as np
from scipy.signal import savgol_filter, find_peaks
from scipy.interpolate import interp1d, pchip_interpolate

try:
    from scipy.interpolate import make_smoothing_spline
except ImportError:
    make_smoothing_spline = None


def smooth(y, win_size=None):
    """
    Savitzky–Golay smoothing. If win_size is None, choose
    ~5% of len + 0.5*(range).
    """
    cr = y.max() - y.min()
    if win_size is None:
        w = int(0.05 * len(y) + 0.5 * cr)
    else:
        w = win_size
    p = 3
    if w <= p:
        w = p + 1
    if w % 2 == 0:
        w += 1
    return savgol_filter(y, w, p)


def find_minima_scipy(y, distance=None, prominence=None):
    """
    Find minima in signal y.

    Improvements over original:
    1. Uses the median gap between peaks to determine the exclusion distance
       (instead of relying only on the first two peaks).
    2. Crucially, it re-applies the 'prominence' constraint in the second pass
       so noise isn't picked up just because it fits the distance.
    """
    # Minima of y => Maxima of -y
    inv_y = -y

    # If user provided a specific distance, trust it and run once.
    if distance is not None:
        peaks, _ = find_peaks(inv_y, distance=distance, prominence=prominence)
        return peaks

    # Auto-estimation mode
    # First pass: Find all potential peaks using just the prominence
    peaks_initial, _ = find_peaks(inv_y, prominence=prominence)

    # We need enough peaks to estimate a period (Kiessig fringe width)
    if len(peaks_initial) < 4:
        return peaks_initial

    # Calculate gaps between consecutive peaks
    gaps = np.diff(peaks_initial)

    # Use the median gap to estimate the fringe period.
    # The median is robust: one missing peak or one noise spike won't break it.
    median_gap = np.median(gaps)

    # Enforce a minimum distance of 60% of the median gap
    auto_distance = int(median_gap * 0.60)
    auto_distance = max(auto_distance, 1)

    # Second pass: Apply the calculated distance AND the original prominence
    peaks_refined, _ = find_peaks(inv_y, distance=auto_distance, prominence=prominence)

    return peaks_refined


def baseline(y, q, minima_idx, method="pchip"):
    """
    Estimate baseline through minima points.
    method: "linear", "spline", or "pchip".
    """
    # Ensure minima_idx are valid integers and within bounds
    minima_idx = np.asarray(minima_idx, dtype=int)
    minima_idx = minima_idx[minima_idx < len(y)]

    if len(minima_idx) == 0:
        return np.zeros_like(y)

    xs = np.insert(q[minima_idx], 0, 0)
    ys = np.insert(y[minima_idx], 0, 0)

    if method == "linear":
        f = interp1d(
            xs, ys, kind="linear", bounds_error=False, fill_value="extrapolate"
        )
        b = f(q)
    elif method == "spline" and make_smoothing_spline and len(xs) >= 5:
        b = make_smoothing_spline(xs, ys)(q)
    else:
        # PCHIP is usually best for XRR baselines as it preserves monotonicity
        b = pchip_interpolate(xs, ys, q)

    b = np.clip(b, 0, y)
    return b


def subtract_baseline(y, b):
    """
    Return |y - b|.
    """
    return np.abs(y - b)


def invert_signs(y, minima_idx):
    """
    Flip the sign of y after each minima index.
    """
    y2 = y.copy()
    minima_idx = np.sort(minima_idx)
    for idx in minima_idx:
        y2[idx:] *= -1
    return y2
