/// Unicode block characters used for sparkline rendering.
/// These map to 8 intensity levels from lowest (U+2581) to highest (U+2588).
const BLOCKS: [char; 8] = [
    '\u{2581}', // Lower one eighth block
    '\u{2582}', // Lower one quarter block
    '\u{2583}', // Lower three eighths block
    '\u{2584}', // Lower half block
    '\u{2585}', // Lower five eighths block
    '\u{2586}', // Lower three quarters block
    '\u{2587}', // Lower seven eighths block
    '\u{2588}', // Full block
];

/// ANSI escape code for green foreground.
const ANSI_GREEN: &str = "\x1b[32m";
/// ANSI escape code for red foreground.
const ANSI_RED: &str = "\x1b[31m";
/// ANSI escape code to reset all styling.
const ANSI_RESET: &str = "\x1b[0m";

/// Generates a sparkline string from numeric values using Unicode block characters.
///
/// Characters used: `▁▂▃▄▅▆▇█` (U+2581 through U+2588). Each value is mapped
/// linearly to one of the 8 block levels based on its position between the
/// minimum and maximum values in the input.
///
/// # Examples
///
/// ```
/// use termichart_charts::sparkline::sparkline;
///
/// let s = sparkline(&[1.0, 3.0, 5.0, 2.0, 8.0]);
/// assert_eq!(s, "▁▃▅▂█");
/// ```
///
/// An empty slice produces an empty string:
///
/// ```
/// use termichart_charts::sparkline::sparkline;
///
/// assert_eq!(sparkline(&[]), "");
/// ```
///
/// A single value always maps to the lowest block:
///
/// ```
/// use termichart_charts::sparkline::sparkline;
///
/// assert_eq!(sparkline(&[42.0]), "\u{2581}");
/// ```
pub fn sparkline(values: &[f64]) -> String {
    if values.is_empty() {
        return String::new();
    }

    let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
    let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let range = max - min;

    let mut result = String::with_capacity(values.len() * 4); // UTF-8 chars can be up to 4 bytes

    for &v in values {
        let idx = if range == 0.0 {
            // All values are identical (or single value); use the lowest block.
            0
        } else {
            // Normalize to [0, 1] then scale to [0, 7].
            let normalized = (v - min) / range;
            let scaled = (normalized * 7.0).round() as usize;
            scaled.min(7)
        };
        result.push(BLOCKS[idx]);
    }

    result
}

/// Generates a colored sparkline with ANSI escape codes.
///
/// Values above the mean are rendered in green; values at or below the mean
/// are rendered in red. The block character mapping is identical to [`sparkline`].
///
/// # Examples
///
/// ```
/// use termichart_charts::sparkline::sparkline_colored;
///
/// let s = sparkline_colored(&[1.0, 5.0, 3.0]);
/// // The output contains ANSI color codes around each character.
/// assert!(s.contains("\x1b[32m")); // green
/// assert!(s.contains("\x1b[31m")); // red
/// ```
///
/// An empty slice produces an empty string:
///
/// ```
/// use termichart_charts::sparkline::sparkline_colored;
///
/// assert_eq!(sparkline_colored(&[]), "");
/// ```
pub fn sparkline_colored(values: &[f64]) -> String {
    if values.is_empty() {
        return String::new();
    }

    let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
    let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let range = max - min;
    let mean = values.iter().sum::<f64>() / values.len() as f64;

    let mut result = String::with_capacity(values.len() * 16); // room for ANSI codes + chars

    for &v in values {
        let idx = if range == 0.0 {
            0
        } else {
            let normalized = (v - min) / range;
            let scaled = (normalized * 7.0).round() as usize;
            scaled.min(7)
        };

        let color = if v > mean { ANSI_GREEN } else { ANSI_RED };
        result.push_str(color);
        result.push(BLOCKS[idx]);
        result.push_str(ANSI_RESET);
    }

    result
}

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

    #[test]
    fn empty_input() {
        assert_eq!(sparkline(&[]), "");
        assert_eq!(sparkline_colored(&[]), "");
    }

    #[test]
    fn single_value() {
        assert_eq!(sparkline(&[42.0]), "\u{2581}");
    }

    #[test]
    fn all_same_values() {
        let s = sparkline(&[5.0, 5.0, 5.0]);
        assert_eq!(s, "\u{2581}\u{2581}\u{2581}");
    }

    #[test]
    fn ascending_values() {
        let s = sparkline(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
        assert_eq!(s, "\u{2581}\u{2582}\u{2583}\u{2584}\u{2585}\u{2586}\u{2587}\u{2588}");
    }

    #[test]
    fn descending_values() {
        let s = sparkline(&[7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0]);
        assert_eq!(s, "\u{2588}\u{2587}\u{2586}\u{2585}\u{2584}\u{2583}\u{2582}\u{2581}");
    }

    #[test]
    fn known_sparkline() {
        // 1.0 -> min -> idx 0 -> ▁
        // 3.0 -> (3-1)/(8-1) = 2/7 ~ 0.286 -> 0.286*7 = 2.0 -> idx 2 -> ▃
        // 5.0 -> (5-1)/(8-1) = 4/7 ~ 0.571 -> 0.571*7 = 4.0 -> idx 4 -> ▅
        // 2.0 -> (2-1)/(8-1) = 1/7 ~ 0.143 -> 0.143*7 = 1.0 -> idx 1 -> ▂
        // 8.0 -> max -> idx 7 -> █
        let s = sparkline(&[1.0, 3.0, 5.0, 2.0, 8.0]);
        assert_eq!(s, "▁▃▅▂█");
    }

    #[test]
    fn colored_contains_ansi_codes() {
        let s = sparkline_colored(&[1.0, 5.0, 3.0]);
        assert!(s.contains(ANSI_GREEN));
        assert!(s.contains(ANSI_RED));
        assert!(s.contains(ANSI_RESET));
    }

    #[test]
    fn colored_max_is_green() {
        // mean of [0.0, 10.0] = 5.0
        // 0.0 <= 5.0 -> red, 10.0 > 5.0 -> green
        let s = sparkline_colored(&[0.0, 10.0]);
        assert!(s.starts_with(ANSI_RED));
        assert!(s.contains(&format!("{ANSI_GREEN}\u{2588}{ANSI_RESET}")));
    }

    #[test]
    fn negative_values() {
        let s = sparkline(&[-3.0, -1.0, 0.0, 2.0]);
        // -3 -> min -> idx 0 -> ▁
        // -1 -> (2/5)*7 = 2.8 -> round 3 -> ▄
        // 0  -> (3/5)*7 = 4.2 -> round 4 -> ▅
        // 2  -> max -> idx 7 -> █
        assert_eq!(s, "\u{2581}\u{2584}\u{2585}\u{2588}");
    }
}
