# SPDX-FileCopyrightText: Copyright © 2024-2026 Damian Anders <damian.anders@tu-dresden.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later

"""
Evaluate and simulate eccentric motion fitting performance under various conditions.

This module provides tools for testing the robustness of runout fitting algorithms
by simulating noisy signals and generating synthetic surface form deviations (roundness errors).
It is primarily used for validation and sensitivity analysis of the 'fit_eccentricity' module.
"""

from typing import Literal

import numpy as np
from numpy import typing as npt
from scipy import optimize as sop

from ._protocols import FitFunction
from .fit_eccentricity import fit_eccentricity

__all__ = ["evaluate_noisy_fit",
		   "getNoiseScaling"]


def evaluate_noisy_fit(time_values: npt.NDArray[np.float64],
					   signal: npt.NDArray[np.float64],
					   noise: npt.NDArray[np.float64],
					   SNR: float,
					   radius: float,
					   fit_function: FitFunction | Literal['cos', 'exc'],
					   bounds: sop.Bounds | None,
					   f_lowpass: float,
) -> sop.OptimizeResult:
	"""
	Evaluates fitting performance by applying controlled noise to a clean signal.

	This function scales a noise vector to reach a target Signal-to-Noise Ratio (SNR),
	adds it to the reference signal, and then performs a runout fit. It is used
	to assess how accurately parameters can be recovered from noisy measurements.

	Parameters
	----------
	time_values : npt.NDArray[np.float64]
		One-dimensional array of time stamps for the measurements.
	signal : npt.NDArray[np.float64]
		The "true" or reference signal data without noise.
	noise : npt.NDArray[np.float64]
		Raw noise data to be scaled and added to the reference signal.
	SNR : float
		Target Signal-to-Noise Ratio (power-based) used to scale `yRauschen`.
	radius : float
		Nominal radius of the rotating object, used for the 'exc' model.
	fit_function : {'exc', 'cos'} or FitFunction
		The model to fit (exact eccentric geometry or cosine approximation).
	bounds : scipy.optimize.Bounds or None
		Boundaries for the optimization parameters.
	f_lowpass : float
		Cutoff frequency in Hz for the internal filtering process.

	Returns
	-------
	scipy.optimize.OptimizeResult
		The result of the `fit_eccentricity` optimization on the noisy signal.

	See Also
	--------
	pyOutRun.fit_eccentricity.fit_eccentricity : The underlying fitting function.
	pyOutRun._signalNoise.getNoiseScaling : Helper for SNR-based scaling.
	"""
	skalFaktor = getNoiseScaling(signal, noise, SNR)		# Skalierungsfaktor für Rauschen ermitteln, mit dem die gewünschte SNR erreicht wird
	yIstRausch = signal + skalFaktor * noise				# Signal mit entsprechend skaliertem Rauschen beaufschlagen

	return fit_eccentricity(time_values, yIstRausch, radius, fit_function, bounds, f_lowpass=f_lowpass)


def _sigPower(signal: npt.NDArray[np.float64],
) -> npt.NDArray[np.float64]:
	"""Signal power"""
	return np.sum(np.square(signal))/signal.size


def _getSNR(signal: npt.NDArray[np.float64],
			noise:npt.NDArray[np.float64],
) -> npt.NDArray[np.float64]:
	"""Calculate SNR from known signal with known noise."""
	Psig = _sigPower(signal)
	Pnoise = _sigPower(noise)
	return 10 * np.log10(Psig/Pnoise)


def getNoiseScaling(signal: npt.NDArray[np.float64],
					noise: npt.NDArray[np.float64],
					SNR: float,
) -> float:
	"""Determine the factor by which the defined SNR can be adjusted by scaling the noise."""
	if np.isposinf(SNR):
		factor = np.float64(0)
	else:
		Psig = _sigPower(signal)
		Pnoise = _sigPower(noise)

		factor = np.sqrt(Psig/Pnoise * np.power(10, -(SNR/10)))

	return factor
