use std::time::Duration;

/// Parse a go formatted duration.
///
/// Sources:
/// - <https://crates.io/crates/go-parse-duration> (fixed an utf8 bug)
/// - <https://github.com/golang/go/blob/master/src/time/format.go>
pub fn parse_go_duration(value: &str) -> Result<Duration, String> {
    parse_duration(value).map(|ns| Duration::from_nanos(ns.unsigned_abs()))
}

fn parse_duration(string: &str) -> Result<i64, String> {
    // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
    let mut s = string;
    let mut d: i64 = 0; // duration to be returned
    let mut neg = false;

    // Consume [-+]?
    if !s.is_empty() {
        let c = *s.as_bytes().first().unwrap();
        if c == b'-' || c == b'+' {
            neg = c == b'-';
            s = &s[1..];
        }
    }
    // Special case: if all that is left is "0", this is zero.
    if s == "0" {
        return Ok(0);
    }
    if s.is_empty() {
        return Err(format!("invalid duration: {string}"));
    }
    while !s.is_empty() {
        // integers before, after decimal point
        let mut v: i64;
        let mut f: i64 = 0;
        // value = v + f / scale
        let mut scale: f64 = 1f64;

        // The next character must be [0-9.]
        let c = *s.as_bytes().first().unwrap();
        if !(c == b'.' || c.is_ascii_digit()) {
            return Err(format!("invalid duration: {string}"));
        }
        // Consume [0-9]*
        let pl = s.len();
        match leading_int(s) {
            Ok((_v, _s)) => {
                v = _v;
                s = _s;
            }
            Err(_) => {
                return Err(format!("invalid duration: {string}"));
            }
        }
        let pre = pl != s.len(); // whether we consume anything before a period

        // Consume (\.[0-9]*)?
        let mut post = false;
        if !s.is_empty() && *s.as_bytes().first().unwrap() == b'.' {
            s = &s[1..];
            let pl = s.len();
            let (f_, scale_, s_) = leading_fraction(s);
            {
                f = f_;
                scale = scale_;
                s = s_;
            }
            post = pl != s.len();
        }
        if !pre && !post {
            // no digits (e.g. ".s" or "-.s")
            return Err(format!("invalid duration: {string}"));
        }

        // Consume unit.
        let mut i = 0;
        while i < s.len() {
            let c = *s.as_bytes().get(i).unwrap();
            if c == b'.' || c.is_ascii_digit() {
                break;
            }
            i += 1;
        }
        if i == 0 {
            return Err(format!("missing unit in duration: {string}"));
        }
        let u = &s[..i];
        s = &s[i..];
        let unit = match u {
            "ns" => 1i64,
            "us" => 1000i64,
            "µs" => 1000i64, // U+00B5 = micro symbol
            "μs" => 1000i64, // U+03BC = Greek letter mu
            "ms" => 1000000i64,
            "s" => 1000000000i64,
            "m" => 60000000000i64,
            "h" => 3600000000000i64,
            _ => {
                return Err(format!("unknown unit {u} in duration {string}"));
            }
        };
        if v > (1 << (63 - 1)) / unit {
            // overflow
            return Err(format!("invalid duration {string}"));
        }
        v *= unit;
        if f > 0 {
            // f64 is needed to be nanosecond accurate for fractions of hours.
            // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
            v += (f as f64 * (unit as f64 / scale)) as i64;
            if v < 0 {
                // overflow
                return Err(format!("invalid duration {string}"));
            }
        }
        d += v;
        if d < 0 {
            // overflow
            return Err(format!("invalid duration {string}"));
        }
    }
    if neg {
        d = -d;
    }
    Ok(d)
}

fn leading_int(s: &str) -> Result<(i64, &str), String> {
    let mut x = 0;
    let mut i = 0;
    while i < s.len() {
        let c = s.chars().nth(i).unwrap();
        if !c.is_ascii_digit() {
            break;
        }
        if x > (1 << (63 - 1)) / 10 {
            return Err("overflow".into());
        }
        let d = i64::from(c.to_digit(10).unwrap());
        x = x * 10 + d;
        if x < 0 {
            // overflow
            return Err("overflow".into());
        }
        i += 1;
    }
    Ok((x, &s[i..]))
}

fn leading_fraction(s: &str) -> (i64, f64, &str) {
    let mut i = 0;
    let mut x = 0i64;
    let mut scale = 1f64;
    let mut overflow = false;
    while i < s.len() {
        let c = s.chars().nth(i).unwrap();
        if !c.is_ascii_digit() {
            break;
        }
        if overflow {
            continue;
        }
        if x > (1 << (63 - 1)) / 10 {
            // It's possible for overflow to give a positive number, so take care.
            overflow = true;
            continue;
        }
        let d = i64::from(c.to_digit(10).unwrap());
        let y = x * 10 + d;
        if y < 0 {
            overflow = true;
            continue;
        }
        x = y;
        scale *= 10f64;
        i += 1;
    }
    (x, scale, &s[i..])
}

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

    #[test]
    fn test_parse_duration() {
        assert_eq!(parse_duration("8.731µs"), Ok(8731));
        assert_eq!(parse_duration("50ns"), Ok(50));
        assert_eq!(parse_duration("3ms"), Ok(3000000));
        assert_eq!(parse_duration("2us"), Ok(2000));
        assert_eq!(parse_duration("4.0s"), Ok(4000000000));
        assert_eq!(parse_duration("1h45m"), Ok(6300000000000));
        assert_eq!(
            parse_duration("1"),
            Err(String::from("missing unit in duration: 1")),
        );
    }
}
