//! Small helpers that preserve the scalar behavior used by the C# value types.

#![allow(clippy::cast_possible_truncation)] // Hashes intentionally fold raw IEEE bit patterns.
#![allow(clippy::cast_possible_wrap)] // .NET hash codes reinterpret unsigned IEEE bits as i32.
#![allow(clippy::float_cmp)] // Signed-zero and exact scalar compatibility require exact comparisons.

use crate::Error;
use std::cmp::Ordering;

pub(crate) fn checked_range(
    pos: i32,
    width: usize,
    len: usize,
) -> Result<std::ops::Range<usize>, Error> {
    let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
    let end = start.checked_add(width).ok_or(Error::IndexOutOfRange)?;
    if end > len {
        return Err(Error::IndexOutOfRange);
    }
    Ok(start..end)
}

pub(crate) fn clamp_f32(value: f32, min: f32, max: f32) -> f32 {
    if value > max {
        max
    } else if value < min {
        min
    } else {
        value
    }
}

pub(crate) fn clamp_f64(value: f64, min: f64, max: f64) -> f64 {
    if value > max {
        max
    } else if value < min {
        min
    } else {
        value
    }
}

pub(crate) fn lerp_f32(value1: f32, value2: f32, amount: f32) -> f32 {
    value1 + (value2 - value1) * amount
}

pub(crate) fn lerp_f64(value1: f64, value2: f64, amount: f64) -> f64 {
    value1 + (value2 - value1) * amount
}

pub(crate) fn smooth_step_f32(value1: f32, value2: f32, amount: f32) -> f32 {
    let amount = clamp_f32(amount, 0.0, 1.0);
    lerp_f32(value1, value2, amount * amount * (3.0 - 2.0 * amount))
}

pub(crate) fn smooth_step_f64(value1: f64, value2: f64, amount: f64) -> f64 {
    let amount = clamp_f64(amount, 0.0, 1.0);
    lerp_f64(value1, value2, amount * amount * (3.0 - 2.0 * amount))
}

pub(crate) fn compare_f32(lhs: f32, rhs: f32) -> i32 {
    match (lhs.is_nan(), rhs.is_nan()) {
        (true, true) => 0,
        (true, false) => -1,
        (false, true) => 1,
        (false, false) => match lhs.partial_cmp(&rhs).expect("non-NaN floats are ordered") {
            Ordering::Less => -1,
            Ordering::Equal => 0,
            Ordering::Greater => 1,
        },
    }
}

pub(crate) fn compare_f64(lhs: f64, rhs: f64) -> i32 {
    match (lhs.is_nan(), rhs.is_nan()) {
        (true, true) => 0,
        (true, false) => -1,
        (false, true) => 1,
        (false, false) => match lhs.partial_cmp(&rhs).expect("non-NaN floats are ordered") {
            Ordering::Less => -1,
            Ordering::Equal => 0,
            Ordering::Greater => 1,
        },
    }
}

pub(crate) fn hash_f32(value: f32) -> i32 {
    let bits = if value == 0.0 {
        0
    } else if value.is_nan() {
        0x7fc0_0000
    } else {
        value.to_bits()
    };
    bits as i32
}

pub(crate) fn hash_f64(value: f64) -> i32 {
    let bits = if value == 0.0 {
        0
    } else if value.is_nan() {
        0x7ff8_0000_0000_0000
    } else {
        value.to_bits()
    };
    ((bits as u32) ^ ((bits >> 32) as u32)) as i32
}

pub(crate) fn max_f32(lhs: f32, rhs: f32) -> f32 {
    if lhs.is_nan() {
        lhs
    } else if rhs.is_nan() {
        rhs
    } else if lhs == rhs {
        if lhs == 0.0 && lhs.is_sign_negative() {
            rhs
        } else {
            lhs
        }
    } else if lhs > rhs {
        lhs
    } else {
        rhs
    }
}

pub(crate) fn min_f32(lhs: f32, rhs: f32) -> f32 {
    if lhs.is_nan() {
        lhs
    } else if rhs.is_nan() {
        rhs
    } else if lhs == rhs {
        if lhs == 0.0 && lhs.is_sign_negative() {
            lhs
        } else {
            rhs
        }
    } else if lhs < rhs {
        lhs
    } else {
        rhs
    }
}

pub(crate) fn max_f64(lhs: f64, rhs: f64) -> f64 {
    if lhs.is_nan() {
        lhs
    } else if rhs.is_nan() {
        rhs
    } else {
        lhs.max(rhs)
    }
}

pub(crate) fn min_f64(lhs: f64, rhs: f64) -> f64 {
    if lhs.is_nan() {
        lhs
    } else if rhs.is_nan() {
        rhs
    } else {
        lhs.min(rhs)
    }
}

pub(crate) fn parse_f32(value: &str) -> Result<f32, Error> {
    value.trim().parse().map_err(|_| Error::Argument)
}

pub(crate) fn parse_f64(value: &str) -> Result<f64, Error> {
    value.trim().parse().map_err(|_| Error::Argument)
}

pub(crate) fn format_f32(value: f32) -> String {
    match value {
        value if value.is_nan() => "NaN".to_owned(),
        value if value == f32::INFINITY => "Infinity".to_owned(),
        value if value == f32::NEG_INFINITY => "-Infinity".to_owned(),
        value => value.to_string().replace('e', "E"),
    }
}

pub(crate) fn format_f64(value: f64) -> String {
    match value {
        value if value.is_nan() => "NaN".to_owned(),
        value if value == f64::INFINITY => "Infinity".to_owned(),
        value if value == f64::NEG_INFINITY => "-Infinity".to_owned(),
        value => value.to_string().replace('e', "E"),
    }
}
