use crate::{quantity::Quantity, span::Span, value::Value};
use compact_str::{CompactString, format_compact};
use std::fmt::Display;
use thiserror::Error;

#[derive(Debug, Clone, Error, PartialEq)]
pub struct AssertEq2Error {
    pub span_lhs: Span,
    pub lhs: Value,
    pub span_rhs: Span,
    pub rhs: Value,
    /// When both values are quantities, stores lhs converted to rhs's unit.
    pub lhs_converted: Option<Quantity>,
    /// When both values are quantities, stores the absolute difference (in rhs's unit).
    pub diff: Option<Quantity>,
}

impl AssertEq2Error {
    /// Returns formatted display strings for lhs and rhs values.
    /// When both values are quantities with different units, lhs is shown
    /// converted to rhs's unit with the original in parentheses.
    pub fn fmt_values(&self) -> (String, String) {
        let rhs_str = format!("{}", self.rhs);
        let lhs_str = if let Some(ref lhs_converted) = self.lhs_converted {
            let lhs_converted_str = format!("{lhs_converted}");
            if let Value::Quantity(ref lhs_q) = self.lhs
                && lhs_converted.unit() != lhs_q.unit()
            {
                return (format!("{lhs_converted_str} ({})", self.lhs), rhs_str);
            }
            lhs_converted_str
        } else {
            format!("{}", self.lhs)
        };
        (lhs_str, rhs_str)
    }

    fn is_floating_point_inaccuracy(&self) -> bool {
        if let Some(ref diff) = self.diff
            && let Some(ref lhs_converted) = self.lhs_converted
        {
            let diff_val = diff.unsafe_value().to_f64().abs();
            let lhs_val = lhs_converted.unsafe_value().to_f64().abs();

            if let Value::Quantity(ref rhs_q) = self.rhs {
                let rhs_val = rhs_q.unsafe_value().to_f64().abs();
                let max_val = lhs_val.max(rhs_val);
                return max_val > 0.0 && diff_val / max_val < 1e-9;
            }
        }
        false
    }
}

impl Display for AssertEq2Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (lhs_str, rhs_str) = self.fmt_values();

        let fp_note = if self.is_floating_point_inaccuracy() {
            "\nNote: this is likely due to floating point inaccuracy. \
            Consider testing approximate equality instead: assert_eq(q1, q2, ε)."
        } else {
            ""
        };

        if let Some(ref diff) = self.diff {
            write!(
                f,
                "Assertion failed because the following two quantities differ by {diff}:\
                \n  {lhs_str}\n  {rhs_str}{fp_note}",
            )
        } else {
            write!(
                f,
                "Assertion failed because the following two values are not the same:\
                \n  {lhs_str}\n  {rhs_str}",
            )
        }
    }
}

#[derive(Debug, Clone, Error, PartialEq)]
pub struct AssertEq3Error {
    pub span_lhs: Span,
    pub lhs_original: Quantity,
    pub lhs_converted: Quantity,
    pub span_rhs: Span,
    pub rhs_original: Quantity,
    pub rhs_converted: Quantity,
    pub eps: Quantity,
    pub diff_abs: Quantity,
}

impl AssertEq3Error {
    /// Returns the precision of epsilon clamped to i8 for compatibility with pretty_dtoa.
    fn eps_precision(&self) -> i8 {
        let (_, eps_precision) = get_float_part_lengths(&self.eps.unsafe_value_as_string());
        // i8 should fit inside usize so this should be safe
        let eps_precision = eps_precision.min(i8::MAX.try_into().unwrap());
        let eps_precision: i8 = eps_precision.try_into().unwrap();
        eps_precision
    }

    /// Returns the input quantity pretty printed with precision matching epsilon.
    pub fn fmt_quantity(&self, quantity: &Quantity) -> String {
        quantity
            .pretty_print_with_precision(self.eps_precision())
            .to_string()
    }

    /// Returns the comparand quantities formatted as strings for pretty error message display.
    pub fn fmt_comparands(&self) -> (CompactString, CompactString) {
        let (lhs_converted_len, _) =
            get_float_part_lengths(&self.lhs_converted.unsafe_value_as_string());
        let (rhs_converted_len, _) =
            get_float_part_lengths(&self.rhs_converted.unsafe_value_as_string());

        let width = core::cmp::Ord::max(lhs_converted_len, rhs_converted_len);

        let lhs = self.fmt_comparand(&self.lhs_converted, &self.lhs_original, width);
        let rhs = self.fmt_comparand(&self.rhs_converted, &self.rhs_original, width);

        (lhs, rhs)
    }

    fn fmt_comparand(
        &self,
        converted: &Quantity,
        original: &Quantity,
        width: usize,
    ) -> CompactString {
        let pretty_converted_str = left_pad_integer_part(
            converted
                .pretty_print_with_precision(self.eps_precision())
                .to_string()
                .as_str(),
            width,
        );

        if converted.unit() == original.unit() {
            pretty_converted_str
        } else {
            format_compact!("{} ({})", pretty_converted_str, original)
        }
    }
}

impl Display for AssertEq3Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (lhs, rhs) = self.fmt_comparands();
        let diff_abs = self.fmt_quantity(&self.diff_abs);
        let eps = self.fmt_quantity(&self.eps);

        write!(
            f,
            "Assertion failed because the following two quantities differ by {}, which is more than {}:\n  {}\n  {}",
            diff_abs, eps, lhs, rhs
        )
    }
}

/// Returns the integer (including negative sign if present) and fractional parts of the input number.
/// The input number should be a float plainly formatted as a string as with `to_string`.
fn get_float_parts(number: &str) -> (&str, &str) {
    let parts: Vec<&str> = number.split('.').collect();
    let integer_part = parts[0];
    let fractional_part = if parts.len() > 1 { parts[1] } else { "" };
    (integer_part, fractional_part)
}

/// Returns the lengths of the integer (including negative sign if present) and fractional parts of the input number.
/// The input number should be a float plainly formatted as a string as with `to_string`.
fn get_float_part_lengths(number: &str) -> (usize, usize) {
    let (integer_part, fractional_part) = get_float_parts(number);
    (integer_part.len(), fractional_part.len())
}

/// Returns the input number padded with 0s until the integer part width (number of characters) is exactly integer_part_width
/// The input number should be a float plainly formatted as a string as with `to_string`.
fn left_pad_integer_part(number: &str, integer_part_width: usize) -> CompactString {
    let (integer_part, fractional_part) = get_float_parts(number);
    let integer_part_len = integer_part.len();
    let is_negative = integer_part.starts_with('-');

    let integer_part_abs = if is_negative {
        // Strip negative sign if present
        &integer_part[1..]
    } else {
        integer_part
    };

    // Calculate padding needed
    let padding_needed = integer_part_width.saturating_sub(integer_part_len);

    // Pad integer part with 0s
    let integer_part_abs_padded = format_compact!(
        "{:0>width$}",
        integer_part_abs,
        width = padding_needed + integer_part_abs.len()
    );

    // Combine the padded integer part and the fractional part
    let padded_str = if fractional_part.is_empty() {
        integer_part_abs_padded
    } else {
        format_compact!("{}.{}", integer_part_abs_padded, fractional_part)
    };

    // Add the negative sign if necessary
    if is_negative {
        format_compact!("-{}", padded_str)
    } else {
        padded_str
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    mod get_float_parts {
        use super::*;
        #[test]
        fn positive_integer() {
            let input = 123f64.to_string();
            let result = get_float_parts(&input);
            assert_eq!(result, ("123", ""));
        }
        #[test]
        fn negative_integer() {
            let input = (-123f64).to_string();
            let result = get_float_parts(&input);
            assert_eq!(result, ("-123", ""));
        }
        #[test]
        fn integer_with_zero_fractional_part() {
            let input = (-123.000f64).to_string();
            let result = get_float_parts(&input);
            assert_eq!(result, ("-123", ""));
        }

        #[test]
        fn positive_number_with_decimal() {
            let input = (123.456f64).to_string();
            let result = get_float_parts(&input);
            assert_eq!(result, ("123", "456"));
        }

        #[test]
        fn negative_number_with_decimal() {
            let input = (-123.456f64).to_string();
            let result = get_float_parts(&input);
            assert_eq!(result, ("-123", "456"));
        }
    }
    mod left_pad {
        use super::*;

        #[test]
        fn positive_number_with_decimal() {
            let result = left_pad_integer_part("123.456", 5);
            assert_eq!(result, "00123.456");
        }

        #[test]
        fn negative_number_with_decimal() {
            let result = left_pad_integer_part("-123.456", 5);
            assert_eq!(result, "-0123.456");
        }

        #[test]
        fn positive_number_without_decimal() {
            let result = left_pad_integer_part("123", 5);
            assert_eq!(result, "00123");
        }

        #[test]
        fn negative_number_without_decimal() {
            let result = left_pad_integer_part("-123", 5);
            assert_eq!(result, "-0123");
        }

        #[test]
        fn number_already_wider_than_width() {
            let result = left_pad_integer_part("12345.6789", 4);
            assert_eq!(result, "12345.6789");
        }

        #[test]
        fn zero_padded() {
            let result = left_pad_integer_part("0.123", 5);
            assert_eq!(result, "00000.123");
        }

        #[test]
        fn negative_zero_padded() {
            let result = left_pad_integer_part("-0.123", 5);
            assert_eq!(result, "-0000.123");
        }
    }
}
