use super::Assessment;

use crate::constants;
use crate::settings::report::BlinkingSettings;

use frc::{Resolution as FrcResolution};

#[derive(Debug)]
struct Score 
{
	drift_value: f64,
	zip_value: f64
}

impl Score
{
	fn value(&self) -> f64
	{
		self.drift_value / self.zip_value
	}

	fn relative_difference(&self) -> f64
	{
		relative_difference(self.drift_value, self.zip_value)
	}
}

fn get_index_closest_to<I: Iterator<Item=f64>>(target: f64, values: I) -> Option<usize>
{
	values.enumerate()
		  .filter(|(_, x)| x.is_finite())
		  .map(|(idx, x)| (idx, (x - target).powi(2).sqrt()))
		  .min_by(|(_, x), (_, y)| x.total_cmp(y))
		  .map(|(idx, _)| idx)
}

fn get_half_correlation(target: f64, qs: &[f64], correlations: &[f64]) -> Option<f64>
{
	get_index_closest_to(target, correlations.iter().copied()).and_then(|idx| qs.get(idx)).copied()
}

fn get_half_correlation_from(target: f64, resolution: &FrcResolution) -> Option<f64>
{
	get_half_correlation(target, resolution.qs(), resolution.correlations())
}

fn relative_difference(x: f64, y: f64) -> f64
{
	let diff = x - y;
	let abs_diff = (diff * diff).sqrt();
	let sum = x + y;
	abs_diff / (sum / 2.0)
}

fn percent_difference(x: f64, y: f64) -> f64
{
	relative_difference(x, y) * 100.0
}

fn get_score(target: f64, drift: &FrcResolution, zip: &FrcResolution) -> Result<Score, String>
{
	let error = |name| format!("Could not calculate spatial frequency at {target} correlation for {name}");
	let drift_value = get_half_correlation_from(target, drift).ok_or_else(|| error(constants::DRIFT_SPLIT))?;
	let zip_value = get_half_correlation_from(target, zip).ok_or_else(|| error(constants::ZIP_SPLIT))?;
	Ok(Score{drift_value, zip_value})
}

fn pass(threshold: f64, relative_difference: f64) -> bool
{
	relative_difference <= threshold
}

fn get_details(pass: bool) -> &'static str
{
	if pass
	{
		""
	}
	else 
	{
		"Mutliple repeat appearances are present. This can be addressed by allowing faster blinking or acquiring for longer"
	}
}

pub fn assess(settings: &BlinkingSettings, drift: &FrcResolution, zip: &FrcResolution) -> Result<Assessment, String>
{
	let score = get_score(settings.target(), drift, zip)?;
	let pass = pass(settings.within_threshold(), score.relative_difference());
	let message = get_details(pass);
	let mut assessment = Assessment::pass_fail("Blinking", pass, message);
	assessment.set_score(score.value());
	Ok(assessment)
}

#[cfg(test)]
mod tests 
{
	use super::*;

	#[test]
	fn can_get_index_basic() 
	{
		let n = 10;
		let result = get_index_closest_to(0.5,  (0..n).map(|idx| idx as f64 / n as f64)).unwrap();
		assert_eq!(result, 5)
	}

	#[test]
	fn can_get_index_backwards() 
	{
		let n = 10;
		let values = (0..n).map(|idx| idx as f64 / n as f64).rev().collect::<Vec<f64>>();
		let result = get_index_closest_to(0.5,  values.iter().copied()).unwrap();
		assert_eq!(result, 4)
	}

	#[test]
	fn resolves_to_first() 
	{
		let result = get_index_closest_to(0.5, [0.4, 0.6].iter().copied()).unwrap();
		assert_eq!(result, 0);
		let result = get_index_closest_to(0.5, [0.6, 0.4].iter().copied()).unwrap();
		assert_eq!(result, 0)
	}

	#[test]
	fn can_handle_nans() 
	{
		let result = get_index_closest_to(0.5, [std::f64::NAN, -std::f64::NAN, 0.6].iter().copied()).unwrap();
		assert_eq!(result, 2);
	}	

	#[test]
	fn can_handle_infs() 
	{
		let result = get_index_closest_to(0.5, [std::f64::INFINITY, -std::f64::INFINITY, 0.6].iter().copied()).unwrap();
		assert_eq!(result, 2);
	}

	#[test]
	fn can_get_half_correlation_basic()
	{
		let qs = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
		let correlations = [1.0, 0.9, 0.5, 0.3, 0.2, 0.1];
		assert_eq!(get_half_correlation(0.5, &qs, &correlations), Some(0.2))
	}

	#[test]
	fn half_correlation_can_handle_different_size_arrays()
	{
		let qs = [0.0];
		let correlations = [];
		assert_eq!(get_half_correlation(0.5, &qs, &correlations), None)
	}

	#[test]
	fn relative_difference_basic()
	{
		assert_eq!(relative_difference(5.0, 7.0), 1.0/3.0)
	}

	#[test]
	fn percent_difference_basic()
	{
		assert_eq!(percent_difference(5.0, 7.0), (1.0/3.0) * 100.0)
	}


	#[test]
	fn score_test_basic()
	{
		let drift_correlation = vec![0.5];
		let drift_qs = vec![1.0];
		let drift_resolution = FrcResolution::new(Ok(0.0), drift_qs, drift_correlation, Vec::new());
		let zip_correlation = vec![0.5];
		let zip_qs = vec![0.5];
		let zip_resolution = FrcResolution::new(Ok(0.0), zip_qs, zip_correlation, Vec::new());
		let result = get_score(0.5, &drift_resolution, &zip_resolution).unwrap();
		assert_eq!(result.value(), 2.0);
		assert_eq!(result.relative_difference(), 2.0 / 3.0)
	}

	#[test]
	fn score_drift_error()
	{
		let drift_correlation = vec![];
		let drift_qs = vec![];
		let drift_resolution = FrcResolution::new(Ok(0.0), drift_qs, drift_correlation, Vec::new());
		let zip_correlation = vec![];
		let zip_qs = vec![];
		let zip_resolution = FrcResolution::new(Ok(0.0), zip_qs, zip_correlation, Vec::new());
		let result = get_score(0.5, &drift_resolution, &zip_resolution).unwrap_err();
		assert_eq!(result, "Could not calculate spatial frequency at 0.5 correlation for drift");
	}

	#[test]
	fn score_zip_error()
	{
		let drift_correlation = vec![0.5];
		let drift_qs = vec![1.0];
		let drift_resolution = FrcResolution::new(Ok(0.0), drift_qs, drift_correlation, Vec::new());
		let zip_correlation = vec![];
		let zip_qs = vec![];
		let zip_resolution = FrcResolution::new(Ok(0.0), zip_qs, zip_correlation, Vec::new());
		let result = get_score(0.5, &drift_resolution, &zip_resolution).unwrap_err();
		assert_eq!(result, "Could not calculate spatial frequency at 0.5 correlation for zip");
	}

	#[test]
	fn pass_is_inclusive()
	{
		assert_eq!(pass(0.5, 0.5), true);
		assert_eq!(pass(0.5, 0.500001), false)
	}

	#[test]
	fn details_on_pass()
	{
		assert_eq!(get_details(true), "")
	}

	#[test]
	fn details_on_fail()
	{
		assert_eq!(get_details(false), "Mutliple repeat appearances are present. This can be addressed by allowing faster blinking or acquiring for longer")
	}
}