use std::fmt;
use std::time::Duration;

use unit_prefix::NumberPrefix;

const SECOND: Duration = Duration::from_secs(1);
const MINUTE: Duration = Duration::from_secs(60);
const HOUR: Duration = Duration::from_secs(60 * 60);
const DAY: Duration = Duration::from_secs(24 * 60 * 60);
const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
const YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60);

/// Wraps an std duration for human basic formatting.
#[derive(Debug)]
pub struct FormattedDuration(pub Duration);

/// Wraps an std duration for human readable formatting.
#[derive(Debug)]
pub struct HumanDuration(pub Duration);

/// Formats bytes for human readability
///
/// # Examples
/// ```rust
/// # use indicatif::HumanBytes;
/// assert_eq!("15 B",     format!("{}", HumanBytes(15)));
/// assert_eq!("1.46 KiB", format!("{}", HumanBytes(1_500)));
/// assert_eq!("1.43 MiB", format!("{}", HumanBytes(1_500_000)));
/// assert_eq!("1.40 GiB", format!("{}", HumanBytes(1_500_000_000)));
/// assert_eq!("1.36 TiB", format!("{}", HumanBytes(1_500_000_000_000)));
/// assert_eq!("1.33 PiB", format!("{}", HumanBytes(1_500_000_000_000_000)));
/// ```
#[derive(Debug)]
pub struct HumanBytes(pub u64);

/// Formats bytes for human readability using SI prefixes
///
/// # Examples
/// ```rust
/// # use indicatif::DecimalBytes;
/// assert_eq!("15 B",    format!("{}", DecimalBytes(15)));
/// assert_eq!("1.50 kB", format!("{}", DecimalBytes(1_500)));
/// assert_eq!("1.50 MB", format!("{}", DecimalBytes(1_500_000)));
/// assert_eq!("1.50 GB", format!("{}", DecimalBytes(1_500_000_000)));
/// assert_eq!("1.50 TB", format!("{}", DecimalBytes(1_500_000_000_000)));
/// assert_eq!("1.50 PB", format!("{}", DecimalBytes(1_500_000_000_000_000)));
/// ```
#[derive(Debug)]
pub struct DecimalBytes(pub u64);

/// Formats bytes for human readability using ISO/IEC prefixes
///
/// # Examples
/// ```rust
/// # use indicatif::BinaryBytes;
/// assert_eq!("15 B",     format!("{}", BinaryBytes(15)));
/// assert_eq!("1.46 KiB", format!("{}", BinaryBytes(1_500)));
/// assert_eq!("1.43 MiB", format!("{}", BinaryBytes(1_500_000)));
/// assert_eq!("1.40 GiB", format!("{}", BinaryBytes(1_500_000_000)));
/// assert_eq!("1.36 TiB", format!("{}", BinaryBytes(1_500_000_000_000)));
/// assert_eq!("1.33 PiB", format!("{}", BinaryBytes(1_500_000_000_000_000)));
/// ```
#[derive(Debug)]
pub struct BinaryBytes(pub u64);

/// Formats counts for human readability using commas
#[derive(Debug)]
pub struct HumanCount(pub u64);

/// Formats counts for human readability using commas for floats
#[derive(Debug)]
pub struct HumanFloatCount(pub f64);

impl fmt::Display for FormattedDuration {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut t = self.0.as_secs();
        let seconds = t % 60;
        t /= 60;
        let minutes = t % 60;
        t /= 60;
        let hours = t % 24;
        t /= 24;
        if t > 0 {
            let days = t;
            write!(f, "{days}d {hours:02}:{minutes:02}:{seconds:02}")
        } else {
            write!(f, "{hours:02}:{minutes:02}:{seconds:02}")
        }
    }
}

// `HumanDuration` should be as intuitively understandable as possible.
// So we want to round, not truncate: otherwise 1 hour and 59 minutes
// would display an ETA of "1 hour" which underestimates the time
// remaining by a factor 2.
//
// To make the precision more uniform, we avoid displaying "1 unit"
// (except for seconds), because it would be displayed for a relatively
// long duration compared to the unit itself. Instead, when we arrive
// around 1.5 unit, we change from "2 units" to the next smaller unit
// (e.g. "89 seconds").
//
// Formally:
// * for n >= 2, we go from "n+1 units" to "n units" exactly at (n + 1/2) units
// * we switch from "2 units" to the next smaller unit at (1.5 unit minus half of the next smaller unit)

impl fmt::Display for HumanDuration {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut idx = 0;
        for (i, &(cur, _, _)) in UNITS.iter().enumerate() {
            idx = i;
            match UNITS.get(i + 1) {
                Some(&next) if self.0.saturating_add(next.0 / 2) >= cur + cur / 2 => break,
                _ => continue,
            }
        }

        let (unit, name, alt) = UNITS[idx];
        let mut t = self.0.div_duration_f64(unit).round() as usize;
        if idx < UNITS.len() - 1 {
            t = Ord::max(t, 2);
        }

        match (f.alternate(), t) {
            (true, _) => write!(f, "{t}{alt}"),
            (false, 1) => write!(f, "{t} {name}"),
            (false, _) => write!(f, "{t} {name}s"),
        }
    }
}

const UNITS: &[(Duration, &str, &str)] = &[
    (YEAR, "year", "y"),
    (WEEK, "week", "w"),
    (DAY, "day", "d"),
    (HOUR, "hour", "h"),
    (MINUTE, "minute", "m"),
    (SECOND, "second", "s"),
];

impl fmt::Display for HumanBytes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match NumberPrefix::binary(self.0 as f64) {
            NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
            NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
        }
    }
}

impl fmt::Display for DecimalBytes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match NumberPrefix::decimal(self.0 as f64) {
            NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
            NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
        }
    }
}

impl fmt::Display for BinaryBytes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match NumberPrefix::binary(self.0 as f64) {
            NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
            NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
        }
    }
}

impl fmt::Display for HumanCount {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use fmt::Write;

        let num = self.0.to_string();
        let len = num.len();
        for (idx, c) in num.chars().enumerate() {
            let pos = len - idx - 1;
            f.write_char(c)?;
            if pos > 0 && pos % 3 == 0 {
                f.write_char(',')?;
            }
        }
        Ok(())
    }
}

impl fmt::Display for HumanFloatCount {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use fmt::Write;

        // Use formatter's precision if provided, otherwise default to 4
        let precision = f.precision().unwrap_or(4);
        let num = format!("{:.*}", precision, self.0);

        let (int_part, frac_part) = match num.split_once('.') {
            Some((int_str, fract_str)) => (int_str, fract_str),
            // No decimal point (e.g. precision 0, or non-finite values like
            // "inf"): `num` is already the rounded integer string, so use it
            // directly. Using `self.0.trunc()` here would drop the rounding
            // and turn e.g. `{:.0}` of 1234.9 into "1,234" instead of "1,235".
            None => (num.as_str(), ""),
        };
        // Keep the optional sign out of the digit grouping, otherwise the '-'
        // is counted as a digit and a stray comma lands right after it
        // (e.g. -100 would render as "-,100").
        let (sign, digits) = match int_part.strip_prefix('-') {
            Some(digits) => ("-", digits),
            None => ("", int_part),
        };
        f.write_str(sign)?;
        let len = digits.len();
        for (idx, c) in digits.chars().enumerate() {
            let pos = len - idx - 1;
            f.write_char(c)?;
            if pos > 0 && pos % 3 == 0 {
                f.write_char(',')?;
            }
        }
        let frac_trimmed = frac_part.trim_end_matches('0');
        if !frac_trimmed.is_empty() {
            f.write_char('.')?;
            f.write_str(frac_trimmed)?;
        }
        Ok(())
    }
}

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

    const MILLI: Duration = Duration::from_millis(1);

    #[test]
    fn human_duration_alternate() {
        for (unit, _, alt) in UNITS {
            assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit)));
        }
    }

    #[test]
    fn human_duration_less_than_one_second() {
        assert_eq!(
            "0 seconds",
            format!("{}", HumanDuration(Duration::from_secs(0)))
        );
        assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI)));
        assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI)));
        assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI)));
        assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI)));
    }

    #[test]
    fn human_duration_less_than_two_seconds() {
        assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI)));
        assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI)));
        assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI)));
    }

    #[test]
    fn human_duration_one_unit() {
        assert_eq!("1 second", format!("{}", HumanDuration(SECOND)));
        assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE)));
        assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR)));
        assert_eq!("24 hours", format!("{}", HumanDuration(DAY)));
        assert_eq!("7 days", format!("{}", HumanDuration(WEEK)));
        assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR)));
    }

    #[test]
    fn human_duration_less_than_one_and_a_half_unit() {
        // this one is actually done at 1.5 unit - half of the next smaller unit - epsilon
        // and should display the next smaller unit
        let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI);
        assert_eq!("89 seconds", format!("{d}"));
        let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI);
        assert_eq!("89 minutes", format!("{d}"));
        let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI);
        assert_eq!("35 hours", format!("{d}"));
        let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI);
        assert_eq!("10 days", format!("{d}"));
        let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI);
        assert_eq!("78 weeks", format!("{d}"));
    }

    #[test]
    fn human_duration_one_and_a_half_unit() {
        // this one is actually done at 1.5 unit - half of the next smaller unit
        // and should still display "2 units"
        let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2);
        assert_eq!("2 minutes", format!("{d}"));
        let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2);
        assert_eq!("2 hours", format!("{d}"));
        let d = HumanDuration(DAY + DAY / 2 - HOUR / 2);
        assert_eq!("2 days", format!("{d}"));
        let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2);
        assert_eq!("2 weeks", format!("{d}"));
        let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2);
        assert_eq!("2 years", format!("{d}"));
    }

    #[test]
    fn human_duration_two_units() {
        assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND)));
        assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE)));
        assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR)));
        assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY)));
        assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK)));
        assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR)));
    }

    #[test]
    fn human_duration_less_than_two_and_a_half_units() {
        let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI);
        assert_eq!("2 seconds", format!("{d}"));
        let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI);
        assert_eq!("2 minutes", format!("{d}"));
        let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI);
        assert_eq!("2 hours", format!("{d}"));
        let d = HumanDuration(2 * DAY + DAY / 2 - MILLI);
        assert_eq!("2 days", format!("{d}"));
        let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI);
        assert_eq!("2 weeks", format!("{d}"));
        let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI);
        assert_eq!("2 years", format!("{d}"));
    }

    #[test]
    fn human_duration_two_and_a_half_units() {
        let d = HumanDuration(2 * SECOND + SECOND / 2);
        assert_eq!("3 seconds", format!("{d}"));
        let d = HumanDuration(2 * MINUTE + MINUTE / 2);
        assert_eq!("3 minutes", format!("{d}"));
        let d = HumanDuration(2 * HOUR + HOUR / 2);
        assert_eq!("3 hours", format!("{d}"));
        let d = HumanDuration(2 * DAY + DAY / 2);
        assert_eq!("3 days", format!("{d}"));
        let d = HumanDuration(2 * WEEK + WEEK / 2);
        assert_eq!("3 weeks", format!("{d}"));
        let d = HumanDuration(2 * YEAR + YEAR / 2);
        assert_eq!("3 years", format!("{d}"));
    }

    #[test]
    fn human_duration_three_units() {
        assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND)));
        assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE)));
        assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR)));
        assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY)));
        assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK)));
        assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR)));
    }

    #[test]
    fn human_count() {
        assert_eq!("42", format!("{}", HumanCount(42)));
        assert_eq!("7,654", format!("{}", HumanCount(7654)));
        assert_eq!("12,345", format!("{}", HumanCount(12345)));
        assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));
    }

    #[test]
    fn human_float_count() {
        assert_eq!("42", format!("{}", HumanFloatCount(42.0)));
        assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0)));
        assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0)));
        assert_eq!(
            "1,234,567,890",
            format!("{}", HumanFloatCount(1234567890.0))
        );
        assert_eq!("42.5", format!("{}", HumanFloatCount(42.5)));
        assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345)));
        assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345)));
        assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321)));
        assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456)));
        assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789)));
        assert_eq!(
            "1,234,567,890.1235",
            format!("{}", HumanFloatCount(1234567890.1234567))
        );
        assert_eq!(
            "1,234,567,890.1234",
            format!("{}", HumanFloatCount(1234567890.1234321))
        );
        assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.1234321)));
        assert_eq!("1,234.1", format!("{:.1}", HumanFloatCount(1234.1234321)));
        assert_eq!("1,234.12", format!("{:.2}", HumanFloatCount(1234.1234321)));
        assert_eq!("1,234.123", format!("{:.3}", HumanFloatCount(1234.1234321)));
        assert_eq!(
            "1,234.1234320999999454215867445",
            format!("{:.25}", HumanFloatCount(1234.1234321))
        );
    }

    #[test]
    fn human_float_count_negative() {
        // The sign must stay out of the digit grouping; otherwise a stray
        // comma lands right after the '-' for sign-aligned lengths.
        assert_eq!("-100", format!("{}", HumanFloatCount(-100.0)));
        assert_eq!("-100,000", format!("{}", HumanFloatCount(-100000.0)));
        assert_eq!("-1,000", format!("{}", HumanFloatCount(-1000.0)));
        assert_eq!("-42.5", format!("{}", HumanFloatCount(-42.5)));
        assert_eq!("-inf", format!("{}", HumanFloatCount(f64::NEG_INFINITY)));
    }

    #[test]
    fn human_float_count_zero_precision_rounds() {
        // With precision 0 the integer part must reflect the *rounded* value,
        // not a truncated one: format!("{:.0}", 1234.9) is "1235", so the
        // human-grouped output must be "1,235" and never the truncated "1,234".
        assert_eq!("1,235", format!("{:.0}", HumanFloatCount(1234.9)));
        assert_eq!("2,000", format!("{:.0}", HumanFloatCount(1999.6)));
        assert_eq!("1,000", format!("{:.0}", HumanFloatCount(999.5)));
        assert_eq!("-1,235", format!("{:.0}", HumanFloatCount(-1234.9)));
        // Values that round down are unaffected.
        assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.4)));
    }
}
