// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use jiff::civil::{Date, Era, ISOWeekDate, Time, Weekday};
use jiff::fmt::strtime::Meridiem;
use jiff::tz::Offset;
use jiff::{SignedDuration, Span, Unit, Zoned};

use crate::ast::{DateMatch, DatePattern, DateToken};
use crate::loader::Context;
use crate::types::{BaseUnit, BigInt, BigRat, DateTime, Dimensionality, Number, Numeric, TimeZone};
use std::iter::Peekable;

fn parse_fixed(value: &str, digits: usize) -> Option<i32> {
    if digits != 0 && value.len() != digits {
        return None;
    }
    i32::from_str_radix(value, 10).ok()
}

fn parse_range(value: &str, digits: usize, range: std::ops::RangeInclusive<i32>) -> Option<i32> {
    parse_fixed(value, digits).filter(|v| range.contains(v))
}

fn numeric_match(
    tok: Option<&DateToken>,
    name: &str,
    digits: usize,
    range: std::ops::RangeInclusive<i32>,
) -> Result<i32, String> {
    let tok = tok.ok_or_else(|| format!("Expected {}-digit {}, got eof", digits, name))?;

    if let DateToken::Number(ref s, None) = tok {
        parse_range(s, digits, range.clone())
            .ok_or(format!("Expected {} in range {:?}, got {}", name, range, s))
    } else {
        Err(format!("Expected {}-digit {}, got {}", digits, name, tok))
    }
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Relative {
    unit: Unit,
    value: i32,
}

pub(crate) struct Fields {
    era: Option<Era>,
    year: Option<i32>,
    iso_year: Option<i32>,
    month: Option<i32>,
    week: Option<i32>,
    weekday: Option<Weekday>,
    day: Option<i32>,
    ordinal: Option<i32>,

    hour: Option<i32>,
    hour12: Option<i32>,
    meridiem: Option<Meridiem>,
    minute: Option<i32>,
    second: Option<i32>,
    nanosecond: Option<i32>,

    is_today: bool,
    is_now: bool,
    relative: Option<Relative>,

    time_zone: Option<TimeZone>,
}

impl Fields {
    fn get_year(&self) -> Option<i32> {
        if let Some(iso_year) = self.iso_year {
            return Some(iso_year);
        }
        let era = self.era.unwrap_or(Era::CE);
        if let Some(year) = self.year {
            match era {
                Era::CE => return Some(year),
                Era::BCE => return Some(-year - 1),
            }
        }
        None
    }

    fn to_ymd_date(&self, year: i32) -> Option<Date> {
        let month = self.month?;
        let day = self.day?;
        Date::new(year as i16, month as i8, day as i8).ok()
    }

    fn to_week_date(&self, year: i32) -> Option<Date> {
        let week = self.week?;
        let weekday = self.weekday?;
        ISOWeekDate::new(year as i16, week as i8, weekday)
            .ok()
            .map(Into::into)
    }

    fn to_ordinal_date(&self, year: i32) -> Option<Date> {
        let ordinal = self.ordinal?;
        let date = Date::new(year as i16, 1, 1).ok()?;
        date.with().day_of_year(ordinal as i16).build().ok()
    }

    fn to_date(&self, current_year: i32) -> Option<Date> {
        let year = self.get_year().unwrap_or(current_year);
        self.to_ymd_date(year)
            .or_else(|| self.to_week_date(year))
            .or_else(|| self.to_ordinal_date(year))
    }

    fn get_hour12(&self) -> Option<i32> {
        let hour12 = self.hour12?;
        let meridiem = self.meridiem?;
        Some(match (hour12, meridiem) {
            (12, Meridiem::AM) => 0,
            (12, Meridiem::PM) => 12,
            (x, Meridiem::AM) => x,
            (x, Meridiem::PM) => 12 + x,
        })
    }

    fn get_hour(&self) -> Option<i32> {
        self.hour.or_else(|| self.get_hour12())
    }

    fn to_time(&self) -> Option<Time> {
        let hour = self.get_hour()?;
        let minute = self.minute.unwrap_or(0);
        let second = self.second.unwrap_or(0);
        let nanos = self.nanosecond.unwrap_or(0);
        Time::new(hour as i8, minute as i8, second as i8, nanos).ok()
    }
}

impl Default for Fields {
    fn default() -> Fields {
        Fields {
            era: None,
            year: None,
            iso_year: None,
            month: None,
            week: None,
            weekday: None,
            day: None,
            ordinal: None,
            hour: None,
            hour12: None,
            meridiem: None,
            minute: None,
            second: None,
            nanosecond: None,
            is_today: false,
            is_now: false,
            relative: None,
            time_zone: None,
        }
    }
}

pub(crate) fn parse_date<I>(
    out: &mut Fields,
    date: &mut Peekable<I>,
    pat: &[DatePattern],
) -> Result<(), String>
where
    I: Iterator<Item = DateToken> + Clone,
{
    use std::borrow::Borrow;

    let tok = date.peek().cloned();

    fn ts<T>(x: Option<T>) -> String
    where
        T: Borrow<DateToken>,
    {
        match x {
            Some(ref x) => format!("`{}`", x.borrow()),
            None => "eof".to_owned(),
        }
    }

    let mut advance = true;

    #[allow(unused_assignments)]
    macro_rules! take {
        ($($pat: pat)|+) => {
            match date.peek().cloned() {
                $(Some($pat))|+ => {date.next().unwrap()},
                x => return Err(format!("Expected {}, got {}", stringify!($($pat)|+), ts(x)))
            }
        };
        ($pat:pat, $var:ident) => {
            match date.peek().cloned() {
                Some($pat) => {date.next(); $var},
                x => return Err(format!("Expected {}, got {}", stringify!($pat), ts(x)))
            }
        }
    }

    let res = match pat.first() {
        None => return Ok(()),
        Some(&DatePattern::Literal(ref l)) => match tok {
            Some(DateToken::Literal(ref s)) if s == l => Ok(()),
            x => Err(format!("Expected `{}`, got {}", l, ts(x))),
        },
        Some(&DatePattern::Match(what)) => match what {
            DateMatch::FullYear => {
                numeric_match(tok.as_ref(), "fullyear", 4, 0..=9999).and_then(|v| {
                    out.year = Some(v);
                    Ok(())
                })
            }
            DateMatch::MonthNum => {
                numeric_match(tok.as_ref(), "monthnum", 2, 1..=12).and_then(|v| {
                    out.month = Some(v);
                    Ok(())
                })
            }
            DateMatch::Day => numeric_match(tok.as_ref(), "day", 0, 1..=31).and_then(|v| {
                out.day = Some(v);
                Ok(())
            }),
            DateMatch::FullDay => numeric_match(tok.as_ref(), "fullday", 2, 1..=31).and_then(|v| {
                out.day = Some(v);
                Ok(())
            }),
            DateMatch::Min => numeric_match(tok.as_ref(), "min", 2, 0..=60).and_then(|v| {
                out.minute = Some(v);
                Ok(())
            }),
            DateMatch::Ordinal => {
                numeric_match(tok.as_ref(), "ordinal", 3, 1..=366).and_then(|v| {
                    out.ordinal = Some(v);
                    Ok(())
                })
            }
            DateMatch::Year => numeric_match(tok.as_ref(), "year", 0, 0..=9999).and_then(|v| {
                out.year = Some(v);
                Ok(())
            }),
            DateMatch::IsoWeek => numeric_match(tok.as_ref(), "isoweek", 2, 1..=53).and_then(|v| {
                out.week = Some(v);
                Ok(())
            }),
            DateMatch::IsoYear => {
                advance = false;
                let x = take!(DateToken::Dash | DateToken::Plus | DateToken::Number(_, None));
                let (sign, num) = match x {
                    DateToken::Dash => (-1, None),
                    DateToken::Plus => (1, None),
                    DateToken::Number(i, None) => (1, Some(i)),
                    _ => unreachable!(),
                };
                let num = match num {
                    Some(x) => x,
                    None => take!(DateToken::Number(x, None), x),
                };
                let value = i32::from_str_radix(&*num, 10);
                if let Ok(value) = value {
                    out.iso_year = Some(value * sign);
                    Ok(())
                } else {
                    Err(format!("Expected isoyear, got out of range value"))
                }
            }
            DateMatch::Era => match tok {
                Some(DateToken::Literal(ref s))
                    if { s.to_lowercase() == "ad" || s.to_lowercase() == "ce" } =>
                {
                    out.era = Some(Era::CE);
                    Ok(())
                }
                Some(DateToken::Literal(ref s))
                    if { s.to_lowercase() == "bc" || s.to_lowercase() == "bce" } =>
                {
                    out.era = Some(Era::BCE);
                    Ok(())
                }
                x => Err(format!("Expected AD/BC or CE/BCE, got {}", ts(x))),
            },
            DateMatch::Hour12 => match tok {
                Some(DateToken::Number(ref s, None)) => {
                    if let Some(value) = parse_range(s, 0, 1..=12) {
                        out.hour12 = Some(value);
                        Ok(())
                    } else {
                        Err(format!("Expected hour12, got out of range value {}", s))
                    }
                }
                x => Err(format!("Expected hour12, got {}", ts(x))),
            },
            DateMatch::Hour24 => match tok {
                Some(DateToken::Number(ref s, None)) => {
                    if let Some(value) = parse_range(s, 0, 0..=23) {
                        out.hour = Some(value);
                        Ok(())
                    } else {
                        Err(format!("Expected hour24, got out of range value {}", s))
                    }
                }
                x => Err(format!("Expected hour24, got {}", ts(x))),
            },
            DateMatch::FullHour12 => match tok {
                Some(DateToken::Number(ref s, None)) => {
                    if let Some(value) = parse_range(s, 2, 1..=12) {
                        out.hour12 = Some(value);
                        Ok(())
                    } else {
                        Err(format!(
                            "Expected 2-digit hour12, got out of range value {}",
                            s
                        ))
                    }
                }
                x => Err(format!("Expected 2-digit hour12, got {}", ts(x))),
            },
            DateMatch::FullHour24 => match tok {
                Some(DateToken::Number(ref s, None)) => {
                    if let Some(value) = parse_range(s, 2, 0..=23) {
                        out.hour = Some(value);
                        Ok(())
                    } else {
                        Err(format!(
                            "Expected 2-digit hour24, got out of range value {}",
                            s
                        ))
                    }
                }
                x => Err(format!("Expected 2-digit hour24, got {}", ts(x))),
            },
            DateMatch::Meridiem => match tok {
                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "am" => {
                    out.meridiem = Some(Meridiem::AM);
                    Ok(())
                }
                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "pm" => {
                    out.meridiem = Some(Meridiem::PM);
                    Ok(())
                }
                x => Err(format!("Expected AM/PM, got {}", ts(x))),
            },
            DateMatch::Sec => match tok {
                Some(DateToken::Number(ref s, None)) => {
                    if let Some(value) = parse_range(s, 2, 0..=60) {
                        out.second = Some(value);
                        Ok(())
                    } else {
                        Err(format!("Expected 2-digit sec in range 0..=60, got {}", s))
                    }
                }
                Some(DateToken::Number(ref s, Some(ref f))) if s.len() == 2 => {
                    let secs = u32::from_str_radix(&**s, 10);
                    let nsecs = u32::from_str_radix(&**f, 10);
                    if let (Ok(secs), Ok(nsecs)) = (secs, nsecs) {
                        let nsecs = nsecs * 10u32.pow(9 - f.len() as u32);
                        out.second = Some(secs as i32);
                        out.nanosecond = Some(nsecs as i32);
                        Ok(())
                    } else {
                        Err(format!("Expected 2-digit sec, got {}.{}", s, f))
                    }
                }
                x => Err(format!("Expected 2-digit sec, got {}", ts(x))),
            },
            DateMatch::Offset => {
                advance = false;
                if let Some(DateToken::Literal(ref s)) = date.peek().cloned() {
                    date.next();
                    let s = s
                        .strip_prefix('[')
                        .and_then(|s| s.strip_suffix(']'))
                        .unwrap_or(s);
                    if let Ok(tz) = TimeZone::get(s) {
                        out.time_zone = Some(tz);
                        Ok(())
                    } else {
                        Err(format!("Invalid timezone {}", s))
                    }
                } else {
                    let s = match take!(DateToken::Plus | DateToken::Dash) {
                        DateToken::Plus => 1,
                        DateToken::Dash => -1,
                        _ => unreachable!(),
                    };
                    let h = take!(DateToken::Number(s, None), s);
                    if let Some(hm) = parse_fixed(&h, 4) {
                        let m = hm % 100;
                        let h = hm / 100;
                        let offset = Offset::from_seconds(s * (h * 3600 + m * 60)).unwrap();
                        out.time_zone = Some(TimeZone::fixed(offset));
                        Ok(())
                    } else if let Ok(h) = i32::from_str_radix(&h, 10) {
                        take!(DateToken::Colon);
                        let m = take!(DateToken::Number(s, None), s);
                        if let Some(m) = parse_range(&m, 2, 0..=59) {
                            let offset = Offset::from_seconds(s * (h * 3600 + m * 60)).unwrap();
                            out.time_zone = Some(TimeZone::fixed(offset));
                            Ok(())
                        } else {
                            Err(format!("Expected 2 digits after : in offset, got {}", m))
                        }
                    } else {
                        Err(format!("Expected offset, got {}", h))
                    }
                }
            }
            DateMatch::MonthName => match tok {
                Some(DateToken::Literal(ref s)) => {
                    let res = match &*s.to_lowercase() {
                        "jan" | "january" => 1,
                        "feb" | "february" => 2,
                        "mar" | "march" => 3,
                        "apr" | "april" => 4,
                        "may" => 5,
                        "jun" | "june" => 6,
                        "jul" | "july" => 7,
                        "aug" | "august" => 8,
                        "sep" | "september" => 9,
                        "oct" | "october" => 10,
                        "nov" | "november" => 11,
                        "dec" | "december" => 12,
                        x => return Err(format!("Unknown month name: {}", x)),
                    };
                    out.month = Some(res);
                    Ok(())
                }
                x => Err(format!("Expected month name, got {}", ts(x))),
            },
            DateMatch::WeekDay => match tok {
                Some(DateToken::Literal(ref s)) => {
                    let res = match &*s.to_lowercase() {
                        "mon" | "monday" => Weekday::Monday,
                        "tue" | "tuesday" => Weekday::Tuesday,
                        "wed" | "wednesday" => Weekday::Wednesday,
                        "thu" | "thursday" => Weekday::Thursday,
                        "fri" | "friday" => Weekday::Friday,
                        "sat" | "saturday" => Weekday::Saturday,
                        "sun" | "sunday" => Weekday::Sunday,
                        x => return Err(format!("Unknown weekday: {}", x)),
                    };
                    out.weekday = Some(res);
                    Ok(())
                }
                x => Err(format!("Expected weekday, got {}", ts(x))),
            },
            DateMatch::Today => match tok {
                Some(DateToken::Literal(ref lit)) if lit == "today" => {
                    out.is_today = true;
                    Ok(())
                }
                x => Err(format!("Expected `today`, got {}", ts(x))),
            },
            DateMatch::Now => match tok {
                Some(DateToken::Literal(ref lit)) if lit == "now" => {
                    out.is_now = true;
                    Ok(())
                }
                x => Err(format!("Expected `now`, got {}", ts(x))),
            },
            DateMatch::Relative => match tok {
                Some(sign @ DateToken::Plus | sign @ DateToken::Dash) => {
                    date.next();
                    let number = match date.peek() {
                        Some(DateToken::Number(n, None)) => n,
                        x => return Err(format!("Expected integer, got {}", ts(x))),
                    };
                    let value = i32::from_str_radix(number, 10)
                        .map_err(|e| format!("Invalid integer: {e}"))?;
                    let value = if sign == DateToken::Dash {
                        -value
                    } else {
                        value
                    };
                    date.next();
                    let unit = match date.peek() {
                        Some(DateToken::Literal(lit)) => match &lit.to_lowercase()[..] {
                            "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => {
                                Unit::Nanosecond
                            }
                            "us" | "µs" | "micro" | "micros" | "microsecond" | "microseconds" => {
                                Unit::Microsecond
                            }
                            "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => {
                                Unit::Millisecond
                            }
                            "s" | "sec" | "secs" | "second" | "seconds" => Unit::Second,
                            "mi" | "min" | "mins" | "minute" | "minutes" => Unit::Minute,
                            "h" | "hr" | "hrs" | "hour" | "hours" => Unit::Hour,
                            "d" | "dy" | "day" | "days" => Unit::Day,
                            "w" | "wk" | "week" | "weeks" => Unit::Week,
                            "mo" | "mon" | "month" | "months" => Unit::Month,
                            "y" | "yr" | "yrs" | "year" | "years" => Unit::Year,
                            "m" => {
                                return Err(format!(
                                    "`m` is ambiguous, did you mean `minutes` or `months`?"
                                ))
                            }
                            _ => return Err(format!("Unknown unit `{}`", lit)),
                        },
                        x => return Err(format!("Expected unit, got {}", ts(x))),
                    };
                    out.relative = Some(Relative { unit, value });
                    Ok(())
                }
                x => Err(format!("Expected + or -, got {}", ts(x))),
            },
        },
        Some(&DatePattern::Optional(ref pats)) => {
            advance = false;
            let mut iter = date.clone();
            if let Ok(()) = parse_date(out, &mut iter, &pats[..]) {
                *date = iter
            }
            Ok(())
        }
        Some(&DatePattern::Dash) => match tok {
            Some(DateToken::Dash) => Ok(()),
            x => Err(format!("Expected `-`, got {}", ts(x))),
        },
        Some(&DatePattern::Colon) => match tok {
            Some(DateToken::Colon) => Ok(()),
            x => Err(format!("Expected `:`, got {}", ts(x))),
        },
        Some(&DatePattern::Space) => match tok {
            Some(DateToken::Space) => Ok(()),
            x => Err(format!("Expected ` `, got {}", ts(x))),
        },
    };
    if advance {
        date.next();
    }
    res.and_then(|_| parse_date(out, date, &pat[1..]))
}

fn attempt(
    now: &DateTime,
    date: &[DateToken],
    pat: &[DatePattern],
) -> Result<DateTime, (String, usize)> {
    let mut parsed = Fields::default();
    let mut iter = date.iter().cloned().peekable();
    let res = parse_date(&mut parsed, &mut iter, pat);
    let count = iter.count();
    let res = if count > 0 && res.is_ok() {
        Err(format!(
            "Expected eof, got {}",
            date[date.len() - count..]
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("")
        ))
    } else {
        res
    };
    res.map_err(|e| (e, count))?;

    let mut date = parsed.to_date(now.year());
    let mut time = parsed.to_time();

    if (parsed.is_now || parsed.is_today) && date.is_none() {
        date = Some(now.dt.date());
    }
    if parsed.is_now && time.is_none() {
        time = Some(now.dt.time());
    }

    let tz = parsed.time_zone.unwrap_or(now.dt.time_zone().clone());

    let dt = match (date, time) {
        (Some(date), Some(time)) => jiff::civil::DateTime::from_parts(date, time),
        (Some(date), None) => date.into(),
        (None, Some(time)) => jiff::civil::DateTime::from_parts(now.dt.date(), time),
        (None, None) => return Err(("Can't make a valid datetime".to_owned(), count)),
    };

    let mut zoned: Zoned = match dt.to_zoned(tz) {
        Ok(zoned) => zoned,
        Err(err) => return Err((err.to_string(), count)),
    };

    if let Some(relative) = parsed.relative {
        let span = Span::new();
        let span = match relative.unit {
            Unit::Year => span.years(relative.value),
            Unit::Month => span.months(relative.value),
            Unit::Week => span.weeks(relative.value),
            Unit::Day => span.days(relative.value),
            Unit::Hour => span.hours(relative.value),
            Unit::Minute => span.minutes(relative.value),
            Unit::Second => span.seconds(relative.value),
            Unit::Millisecond => span.milliseconds(relative.value),
            Unit::Microsecond => span.microseconds(relative.value),
            Unit::Nanosecond => span.nanoseconds(relative.value),
        };
        zoned = match zoned.checked_add(span) {
            Ok(zoned) => zoned,
            Err(err) => return Err((err.to_string(), count)),
        };
    }

    Ok(zoned.into())
}

pub fn try_decode(date: &[DateToken], context: &Context) -> Result<DateTime, String> {
    let mut best = None;
    for pat in &context.registry.datepatterns {
        match attempt(&context.now, date, pat) {
            Ok(datetime) => return Ok(datetime),
            Err((e, c)) => {
                //println!("{}", e);
                let better = if let Some((count, _, _)) = best {
                    c < count
                } else {
                    true
                };
                if better {
                    best = Some((c, pat, e.clone()));
                }
            }
        }
    }
    if let Some((_, pat, err)) = best {
        Err(format!(
            "Most likely pattern `{}` failed: {}",
            DatePattern::show(pat),
            err
        ))
    } else {
        Err("Invalid date literal".to_string())
    }
}

pub fn to_duration(num: &Number) -> Result<SignedDuration, String> {
    if num.unit != Dimensionality::base_unit(BaseUnit::new("s")) {
        return Err("Expected seconds".to_string());
    }
    let max = Numeric::from(i64::max_value());
    if num.value.abs() > max {
        return Err(format!(
            "Implementation error: Duration is too large, max is 2^63 seconds",
        ));
    }
    let (seconds, rem) = num.value.div_rem(&Numeric::from(1));
    let nanos = &rem * &Numeric::from(1_000_000_000);
    let seconds = seconds.to_int().unwrap();
    let nanos = nanos.to_int().unwrap();
    Ok(SignedDuration::new(seconds, nanos as i32))
}

pub fn from_duration(duration: &SignedDuration) -> Result<Number, String> {
    let seconds = duration.as_secs();
    let nanos = duration.subsec_nanos();
    let seconds_div = BigInt::one();
    let nanos_div = BigInt::from(1_000_000_000u64);
    let seconds = BigRat::ratio(&BigInt::from(seconds), &seconds_div);
    let nanos = BigRat::ratio(&BigInt::from(nanos), &nanos_div);
    Ok(Number::new_unit(
        Numeric::Rational(&seconds + &nanos),
        BaseUnit::new("s"),
    ))
}

pub fn parse_datepattern<I>(iter: &mut Peekable<I>) -> Result<Vec<DatePattern>, String>
where
    I: Iterator<Item = char>,
{
    let mut out = vec![];
    while iter.peek().is_some() {
        let res = match iter.peek().cloned().unwrap() {
            '-' => DatePattern::Dash,
            ':' => DatePattern::Colon,
            '[' => {
                iter.next();
                let res = DatePattern::Optional(parse_datepattern(iter)?);
                if iter.peek().cloned() != Some(']') {
                    return Err("Expected ]".to_string());
                } else {
                    res
                }
            }
            ']' => break,
            '\'' => {
                iter.next();
                let mut buf = String::new();
                while let Some(c) = iter.peek().cloned() {
                    if c == '\'' {
                        break;
                    } else {
                        iter.next();
                        buf.push(c);
                    }
                }
                DatePattern::Literal(buf)
            }
            // Separator that allows putting patterns next to each other without a space.
            '&' => {
                iter.next();
                continue;
            }
            x if x.is_whitespace() => {
                while iter.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
                    iter.next();
                }
                out.push(DatePattern::Space);
                continue;
            }
            x if x.is_alphabetic() => {
                let mut buf = String::new();
                while let Some(c) = iter.peek().cloned() {
                    if c.is_alphanumeric() {
                        iter.next();
                        buf.push(c);
                    } else {
                        break;
                    }
                }
                match DateMatch::from_str(&buf) {
                    Some(dm) => out.push(DatePattern::Match(dm)),
                    None => return Err(format!("Unknown date pattern `{}`", buf)),
                }
                continue;
            }
            x => return Err(format!("Unrecognized character {}", x)),
        };
        out.push(res);
        iter.next();
    }
    Ok(out)
}

pub fn parse_datefile(file: &str) -> Vec<Vec<DatePattern>> {
    let mut defs = vec![];
    for (num, line) in file.lines().enumerate() {
        let line = line.split('#').next().unwrap();
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        let res = parse_datepattern(&mut line.chars().peekable());
        match res {
            Ok(res) => defs.push(res),
            Err(e) => println!("Line {}: {}: {}", num, e, line),
        }
    }
    defs
}

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

    fn pattern(s: &str) -> Vec<DatePattern> {
        parse_datepattern(&mut s.chars().peekable()).unwrap()
    }

    fn parse_with_tz(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Fields) {
        let mut parsed = Fields::default();
        let pat = pattern(pat);
        let res = parse_date(&mut parsed, &mut date.into_iter().peekable(), &pat);

        (res, parsed)
    }

    fn parse(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Fields) {
        let (res, parsed) = parse_with_tz(date, pat);
        (res, parsed)
    }

    #[test]
    fn test_literal() {
        let date = vec![DateToken::Literal("abc".into())];
        let (res, _parsed) = parse(date.clone(), "'abc'");
        assert!(res.is_ok());

        let (res, _parsed) = parse(date, "'def'");
        assert_eq!(res, Err("Expected `def`, got `abc`".into()));
    }

    #[test]
    fn test_year_plus() {
        let date = vec![DateToken::Plus, DateToken::Number("123".into(), None)];
        let (res, parsed) = parse(date.clone(), "isoyear");
        assert!(res.is_ok());
        assert_eq!(parsed.iso_year, Some(123));

        let date = vec![DateToken::Number("123".into(), None)];
        let (res, parsed2) = parse(date.clone(), "year");
        assert!(res.is_ok());
        assert_eq!(parsed2.year, Some(123));
    }

    #[test]
    fn test_complicated_date_input() {
        let date = vec![
            DateToken::Number("2".to_owned(), None),
            DateToken::Space,
            DateToken::Literal("Pm".into()),
            DateToken::Dash,
            DateToken::Number("05".to_owned(), None),
            DateToken::Colon,
            DateToken::Number("123".to_owned(), None),
            DateToken::Space,
            DateToken::Number("01".to_owned(), None),
            DateToken::Dash,
            DateToken::Number("57".to_owned(), None),
            DateToken::Space,
            DateToken::Literal("May".into()),
        ];

        let (res, parsed) = parse(date, "day meridiem-monthnum:year hour12-min monthname");
        assert_eq!(res, Ok(()));
        assert_eq!(parsed.year, Some(123));
        assert_eq!(parsed.month, Some(5));
        assert_eq!(parsed.day, Some(2));
        assert_eq!(parsed.hour12, Some(1));
        assert_eq!(parsed.meridiem, Some(Meridiem::PM));
        assert_eq!(parsed.get_hour(), Some(13));
        assert_eq!(parsed.minute, Some(57));
    }

    #[test]
    fn ad_bc() {
        let date = vec![
            DateToken::Number("100".to_owned(), None),
            DateToken::Space,
            DateToken::Literal("bce".into()),
            DateToken::Space,
            DateToken::Number("07".to_owned(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];

        let (res, parsed) = parse(date, "year adbc hour12 meridiem");
        assert!(res.is_ok(), "{}", res.unwrap_err());
        assert_eq!(parsed.year, Some(100));
        assert_eq!(parsed.era, Some(Era::BCE));
        assert_eq!(parsed.get_year(), Some(-101));
        assert_eq!(parsed.hour12, Some(7));
        assert_eq!(parsed.meridiem, Some(Meridiem::AM));
    }

    #[test]
    fn ad_bc_wrong() {
        for date in vec![
            vec![DateToken::Literal("foo".into())],
            vec![DateToken::Plus],
        ] {
            let (res, _) = parse(date, "adbc");
            assert!(res.is_err());
        }
    }

    #[test]
    fn short_hour() {
        let date = vec![DateToken::Number("7".into(), None)];
        let (res, _) = parse(date, "hour24");
        assert_eq!(res, Ok(()));
    }

    #[test]
    fn wrong_length_24h() {
        let date = vec![DateToken::Number("7".into(), None)];
        let (res, _) = parse(date, "fullhour24");
        assert_eq!(
            res,
            Err(format!("Expected 2-digit hour24, got out of range value 7"))
        );
    }

    #[test]
    fn test_24h() {
        let (res, parsed) = parse(vec![DateToken::Number("23".into(), None)], "hour24");
        assert!(res.is_ok());
        assert_eq!(parsed.hour, Some(23));
    }

    #[test]
    fn seconds() {
        let date = vec![DateToken::Number("27".into(), Some("000012345".into()))];
        let (res, parsed) = parse(date, "sec");
        assert!(res.is_ok());
        assert_eq!(parsed.second, Some(27));
        assert_eq!(parsed.nanosecond, Some(12345));

        let date = vec![DateToken::Number("27".into(), None)];
        let (res, parsed) = parse(date, "sec");
        assert!(res.is_ok());
        assert_eq!(parsed.second, Some(27));
        assert_eq!(parsed.nanosecond, None);
    }

    #[test]
    fn test_offset() {
        let date = vec![DateToken::Plus, DateToken::Number("0200".into(), None)];
        let (res, parsed) = parse(date, "offset");
        assert!(res.is_ok());
        assert_eq!(
            parsed.time_zone,
            Some(TimeZone::fixed(Offset::from_seconds(2 * 3600).unwrap()))
        );

        let date = vec![
            DateToken::Dash,
            DateToken::Number("01".into(), None),
            DateToken::Colon,
            DateToken::Number("23".into(), None),
        ];
        let (res, parsed) = parse(date, "offset");
        assert!(res.is_ok());
        assert_eq!(
            parsed.time_zone,
            Some(TimeZone::fixed(
                Offset::from_seconds(-(1 * 60 + 23) * 60).unwrap()
            ))
        );

        let date = vec![DateToken::Literal("Europe/London".into())];
        let (res, parsed) = parse_with_tz(date, "offset");
        assert!(res.is_ok(), "{}", res.unwrap_err());
        assert_eq!(
            parsed.time_zone,
            Some(TimeZone::get("Europe/London").unwrap())
        );
    }

    #[test]
    fn test_weekday() {
        let date = vec![DateToken::Literal("saturday".into())];
        let (res, parsed) = parse(date, "weekday");
        assert!(res.is_ok());
        assert_eq!(parsed.weekday, Some(Weekday::Saturday));

        let date = vec![DateToken::Literal("sun".into())];
        assert!(parse(date, "weekday").0.is_ok());

        let date = vec![DateToken::Literal("snu".into())];
        assert_eq!(parse(date, "weekday").0, Err("Unknown weekday: snu".into()));
    }

    #[test]
    fn test_monthname() {
        for (i, &s) in [
            "jan", "feb", "mar", "apr", "may", "june", "jul", "AUGUST", "SEp", "Oct", "novemBer",
            "dec",
        ]
        .iter()
        .enumerate()
        {
            let date = vec![DateToken::Literal(s.into())];
            let (res, parsed) = parse(date, "monthname");
            assert!(res.is_ok());
            assert_eq!(parsed.month, Some(i as i32 + 1));
        }

        let date = vec![DateToken::Literal("foobar".into())];
        let (res, parsed) = parse(date, "monthname");
        assert_eq!(res, Err("Unknown month name: foobar".into()));
        assert_eq!(parsed.month, None);
    }

    #[test]
    fn test_parse_datepattern() {
        use self::DatePattern::*;

        fn parse(s: &str) -> Result<Vec<DatePattern>, String> {
            parse_datepattern(&mut s.chars().peekable())
        }

        assert_eq!(
            parse("-:['abc']"),
            Ok(vec![Dash, Colon, Optional(vec![Literal("abc".into())])])
        );
        assert!(parse("-:['abc'").is_err());
        assert!(parse("*").is_err());
    }

    #[test]
    fn test_attempt() {
        use self::DateToken::*;
        fn n(x: &str) -> DateToken {
            Number(x.into(), None)
        }

        let now = DateTime::default();

        macro_rules! check_attempt {
            ($date:expr, $pat:expr) => {{
                let pat = parse_datepattern(&mut $pat.chars().peekable()).unwrap();
                attempt(&now, $date, pat.as_ref())
            }};
        }

        let tz = Literal("Europe/London".into());

        let date = &[n("23"), Space, n("05"), Space, tz.clone()];
        let res = check_attempt!(date, "hour24 min offset");
        assert!(res.is_ok(), "{:?}", res);

        let date = &[n("23"), Space, tz.clone()];
        let res = check_attempt!(date, "hour24 offset");
        let res = res.expect("should have parsed");
        assert_eq!(res.hour(), 23);
        assert_eq!(res.dt.time_zone(), &TimeZone::get("Europe/London").unwrap());

        let date = &[n("2018"), Space, n("01"), Space, n("01"), Space, tz.clone()];
        let res = check_attempt!(date, "year monthnum day offset");
        assert!(res.is_ok(), "{:?}", res);
    }

    #[test]
    fn test_ordinal() {
        let date = vec![DateToken::Number("227".into(), None)];
        let (res, parsed) = parse(date, "ordinal");
        assert!(res.is_ok());
        assert_eq!(
            parsed.to_ordinal_date(2025),
            Some(Date::new(2025, 8, 15).unwrap())
        );
    }

    #[test]
    fn test_week_date() {
        let date = vec![
            DateToken::Literal("fri".into()),
            DateToken::Space,
            DateToken::Number("33".into(), None),
        ];
        let (res, parsed) = parse(date, "weekday isoweek");
        assert!(res.is_ok());
        assert_eq!(
            parsed.to_week_date(2025),
            Some(Date::new(2025, 8, 15).unwrap())
        );
    }

    #[test]
    fn test_meridiem() {
        let date = vec![
            DateToken::Number("8".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, parsed) = parse(date, "hour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(8, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("8".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("pm".into()),
        ];
        let (res, parsed) = parse(date, "hour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(20, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("12".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, parsed) = parse(date, "hour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(0, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("12".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("pm".into()),
        ];
        let (res, parsed) = parse(date, "hour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(12, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("12".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("asdf".into()),
        ];
        let (res, _parsed) = parse(date, "hour12:min meridiem");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Number("13".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, _parsed) = parse(date, "hour12:min meridiem");
        assert!(!res.is_ok());
    }

    #[test]
    fn test_full_hour12() {
        let date = vec![
            DateToken::Number("08".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, parsed) = parse(date, "fullhour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(8, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("8".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, _parsed) = parse(date, "fullhour12:min meridiem");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Literal("b".into()),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, _parsed) = parse(date, "fullhour12:min meridiem");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Number("08".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("pm".into()),
        ];
        let (res, parsed) = parse(date, "fullhour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(20, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("12".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];
        let (res, parsed) = parse(date, "fullhour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(0, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("12".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
            DateToken::Space,
            DateToken::Literal("pm".into()),
        ];
        let (res, parsed) = parse(date, "fullhour12:min meridiem");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(12, 0, 0, 0).unwrap()));
    }

    #[test]
    fn test_full_hour24() {
        let date = vec![
            DateToken::Number("23".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
        ];
        let (res, parsed) = parse(date, "fullhour24");
        assert!(res.is_ok());
        assert_eq!(parsed.to_time(), Some(Time::new(23, 0, 0, 0).unwrap()));

        let date = vec![
            DateToken::Number("9".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
        ];
        let (res, _parsed) = parse(date, "fullhour24");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Number("25".into(), None),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
        ];
        let (res, _parsed) = parse(date, "fullhour24");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Literal("b".into()),
            DateToken::Colon,
            DateToken::Number("00".into(), None),
        ];
        let (res, _parsed) = parse(date, "fullhour24");
        assert!(!res.is_ok());
    }

    #[test]
    fn test_isoyear() {
        let date = vec![DateToken::Dash, DateToken::Number("7".into(), None)];
        let (res, parsed) = parse(date, "isoyear");
        assert!(res.is_ok());
        assert_eq!(parsed.iso_year, Some(-7));

        let date = vec![DateToken::Number("0".into(), None)];
        let (res, parsed) = parse(date, "isoyear");
        assert!(res.is_ok());
        assert_eq!(parsed.iso_year, Some(0));

        let date = vec![DateToken::Plus, DateToken::Number("2025".into(), None)];
        let (res, parsed) = parse(date, "isoyear");
        assert!(res.is_ok());
        assert_eq!(parsed.iso_year, Some(2025));
    }

    #[test]
    fn test_today() {
        let date = vec![DateToken::Literal("today".into())];
        let (res, parsed) = parse(date, "today");
        assert!(res.is_ok());
        assert_eq!(parsed.is_today, true);

        let date = vec![DateToken::Literal("am".into())];
        let (res, parsed) = parse(date, "today");
        assert!(!res.is_ok());
        assert_eq!(parsed.is_today, false);
    }

    #[test]
    fn test_now() {
        let date = vec![DateToken::Literal("now".into())];
        let (res, parsed) = parse(date, "now");
        assert!(res.is_ok());
        assert_eq!(parsed.is_now, true);

        let date = vec![DateToken::Literal("am".into())];
        let (res, parsed) = parse(date, "now");
        assert!(!res.is_ok());
        assert_eq!(parsed.is_now, false);
    }

    #[test]
    fn test_relative() {
        let date = vec![
            DateToken::Plus,
            DateToken::Number("3".into(), None),
            DateToken::Literal("d".into()),
        ];
        let (res, parsed) = parse(date, "relative");
        assert!(res.is_ok());
        assert_eq!(
            parsed.relative,
            Some(Relative {
                unit: Unit::Day,
                value: 3,
            })
        );

        let date = vec![
            DateToken::Dash,
            DateToken::Number("7".into(), None),
            DateToken::Literal("w".into()),
        ];
        let (res, parsed) = parse(date, "relative");
        assert!(res.is_ok());
        assert_eq!(
            parsed.relative,
            Some(Relative {
                unit: Unit::Week,
                value: -7,
            })
        );

        let cases = [
            (Unit::Nanosecond, "ns"),
            (Unit::Nanosecond, "nano"),
            (Unit::Nanosecond, "nanos"),
            (Unit::Nanosecond, "nanosecond"),
            (Unit::Nanosecond, "nanoseconds"),
            (Unit::Microsecond, "us"),
            (Unit::Microsecond, "µs"),
            (Unit::Microsecond, "micro"),
            (Unit::Microsecond, "micros"),
            (Unit::Microsecond, "microsecond"),
            (Unit::Microsecond, "microseconds"),
            (Unit::Millisecond, "ms"),
            (Unit::Millisecond, "milli"),
            (Unit::Millisecond, "millis"),
            (Unit::Millisecond, "millisecond"),
            (Unit::Millisecond, "milliseconds"),
            (Unit::Second, "s"),
            (Unit::Second, "sec"),
            (Unit::Second, "secs"),
            (Unit::Second, "second"),
            (Unit::Second, "seconds"),
            (Unit::Minute, "mi"),
            (Unit::Minute, "min"),
            (Unit::Minute, "mins"),
            (Unit::Minute, "minute"),
            (Unit::Minute, "minutes"),
            (Unit::Hour, "h"),
            (Unit::Hour, "hr"),
            (Unit::Hour, "hrs"),
            (Unit::Hour, "hour"),
            (Unit::Hour, "hours"),
            (Unit::Day, "d"),
            (Unit::Day, "dy"),
            (Unit::Day, "day"),
            (Unit::Day, "days"),
            (Unit::Week, "w"),
            (Unit::Week, "wk"),
            (Unit::Week, "week"),
            (Unit::Week, "weeks"),
            (Unit::Month, "mo"),
            (Unit::Month, "mon"),
            (Unit::Month, "month"),
            (Unit::Month, "months"),
            (Unit::Year, "y"),
            (Unit::Year, "yr"),
            (Unit::Year, "yrs"),
            (Unit::Year, "year"),
            (Unit::Year, "years"),
        ];

        for (unit, name) in cases {
            let date = vec![
                DateToken::Dash,
                DateToken::Number("2".into(), None),
                DateToken::Literal(name.into()),
            ];
            let (res, parsed) = parse(date, "relative");
            assert!(res.is_ok());
            assert_eq!(parsed.relative, Some(Relative { unit, value: -2 }));
        }

        // Error cases
        let date = vec![
            DateToken::Dash,
            DateToken::Number("5".into(), None),
            DateToken::Literal("m".into()),
        ];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Dash,
            DateToken::Number("5".into(), None),
            DateToken::Literal("asdf".into()),
        ];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![DateToken::Dash, DateToken::Number("5".into(), None)];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![DateToken::Dash];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![DateToken::Dash, DateToken::Literal("hr".into())];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Number("1".into(), None),
            DateToken::Literal("hr".into()),
        ];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());

        let date = vec![
            DateToken::Plus,
            DateToken::Number("1".into(), Some("5".into())),
            DateToken::Literal("hr".into()),
        ];
        let (res, _parsed) = parse(date, "relative");
        assert!(!res.is_ok());
    }

    fn dt(input: &str) -> DateTime {
        input.parse::<Zoned>().unwrap().into()
    }

    fn now_plus_xy(value: i32, unit: &str) -> [DateToken; 4] {
        [
            DateToken::Literal("now".into()),
            if value > 0 {
                DateToken::Plus
            } else {
                DateToken::Dash
            },
            DateToken::Number(format!("{}", value.abs()), None),
            DateToken::Literal(unit.into()),
        ]
    }

    #[test]
    fn test_attempt_relative() {
        let now = dt("2016-08-02 15:33:19[America/New_York]");
        let pattern = &[
            DatePattern::Match(DateMatch::Now),
            DatePattern::Match(DateMatch::Relative),
        ];

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "ns"), pattern),
            Ok(dt("2016-08-02 15:33:19.000000001[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "us"), pattern),
            Ok(dt("2016-08-02 15:33:19.000001[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "ms"), pattern),
            Ok(dt("2016-08-02 15:33:19.001[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "s"), pattern),
            Ok(dt("2016-08-02 15:33:20[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "min"), pattern),
            Ok(dt("2016-08-02 15:34:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(-1, "min"), pattern),
            Ok(dt("2016-08-02 15:32:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "hr"), pattern),
            Ok(dt("2016-08-02 16:33:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "d"), pattern),
            Ok(dt("2016-08-03 15:33:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "w"), pattern),
            Ok(dt("2016-08-09 15:33:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "mon"), pattern),
            Ok(dt("2016-09-02 15:33:19[America/New_York]"))
        );

        assert_eq!(
            attempt(&now, &now_plus_xy(1, "y"), pattern),
            Ok(dt("2017-08-02 15:33:19[America/New_York]"))
        );
    }
}
