lazy_static::lazy_static! {
    static ref RE_ISO8601_DATETIME_OFFSET: Regex = Regex::new(r"^(?P<year>\d{4})-?(?P<month>\d{2})-?(?P<day>\d{2})(?P<prefix>[tT])(?P<hour>\d{1,2}):?(?P<minute>\d{2})?(:?(?P<second>\d{2})(\.?(?P<nano>\d{1,10}))?)?(?P<tz>[Z ]?([+-]\d{1,2}(?P<offset_colon>:?)(\d{2})?)?)?$").unwrap();
    static ref RE_DATE_YMD: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<month>\d{1,2})?(-(?P<day>\d{1,2}))?$"#).unwrap();
    static ref RE_DATE_YO: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<ordinal>\d{3})?$"#).unwrap();
    static ref RE_DATE_YM: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<month>\d{1,2})?$"#).unwrap();
    static ref RE_TIME_HMS_OFFSET_MERIDAN: Regex = Regex::new(r#"^(?P<prefix>[\stT]?)?(?P<hour>\d{1,2})[ :]?(?P<minute>\d{2})?:?((?P<second>\d{2})(\.?(?P<nano>\d{1,10}))?)?:?\s?(?P<meridan>[apAP]\.?[mM]\.?)?\s?(?P<tz>[zZ]|[+-]\d{1,2}:?(\d{2})?)?$"#).unwrap();
    static ref RE_TIME_OFFSET: Regex = Regex::new(r#"^(?P<tz>[zZ]|[+-]\d{1,2}(:?\d{2})?)$"#).unwrap();
    static ref RE_OFFSET_RANGE: Regex = Regex::new(r"^(?P<hour>\d{1,2})(?P<offset_colon>:?)?(?P<minute>\d{2})?$").unwrap();
    static ref RE_DURATION: Regex = Regex::new(r#"^P(?P<period>((?P<year>\d{1,2})[Yy])?-?(((?P<month>\d{1,2})[Mm])-?)?((?P<week>\d{1,2})[Ww])?-?((?P<day>\d{1,2})[dd])?)?T?(?P<time>((?P<hour>\d{1,2})[Hh]?):?((?P<minute>:?\d{1,2})[Mm]?)?:?((?P<second>\d{1,2})[Ss]?)?)?$"#).unwrap();
}
use std::num::ParseIntError;

use serde::Deserialize;
use time::macros::format_description;

use super::*;

#[test]
fn regex_iso8601_datetime_offset() {
    assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27+00:00"));
    assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27.100+00:00"));
    assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27Z"));
    assert!(RE_ISO8601_DATETIME_OFFSET.is_match("20220207T192227Z"));
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("The offset {0} was invalid.")]
    InvalidOffset(String),
    #[error("The time {0} was invalid.")]
    InvalidTime(String),
    #[error("The date {0} was invalid.")]
    InvalidDate(String),
    #[error("The value {0} could not be parsed as a ISO8601/RFC3339 datetime string.")]
    NonISO8601DateTime(String),
    #[error("The value {0} could not be parsed as a RFC2282 datetime string.")]
    NonRfc2822DateTime(String),
    #[error("The value {0} could not be parsed as a datetime, date, time or offset.")]
    CompletelyInvalid(String),
    #[error("The time {0} is missing an hour value.")]
    MissingHour(String),
    #[error("The time {0} is missing an month value.")]
    MissingMonth(String),
    #[error("The time {0} is missing an dayvalue.")]
    MissingDay(String),
    #[error("The oridinal date {0} is missing an year value.")]
    MissingYear(String),
    #[error("The ordinal date {0} is missing an day value.")]
    MissingOrdinalDay(String),
    #[error("Could not parsed as an integer: {0}")]
    Integer(#[from] ParseIntError),
    #[error(transparent)]
    Time(#[from] time::Error),
}

impl std::cmp::PartialEq for Error {
    fn eq(&self, other: &Self) -> bool {
        std::mem::discriminant(self) == std::mem::discriminant(other)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Offset {
    pub data: time::UtcOffset,
    with_minutes: bool,
    with_colon: bool,
}

impl PartialOrd for Offset {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Offset {
    fn cmp(&self, other: &Self) -> Ordering {
        self.data.cmp(&other.data)
    }
}

impl ToString for Offset {
    fn to_string(&self) -> String {
        if self.data.is_utc() {
            "Z".to_string()
        } else {
            let hour = self.data.whole_hours();
            let minute = self.data.minutes_past_hour().abs();
            format!(
                "{hour:+03}{}{}",
                if self.with_colon { ":" } else { "" },
                if minute == 0 && !self.with_minutes {
                    "".into()
                } else {
                    format!("{minute:0>2}")
                }
            )
        }
    }
}

impl FromStr for Offset {
    type Err = Error;

    /// Converts the provided string into a representation of an offset.
    ///
    /// ```
    /// # use std::str::FromStr;
    /// # use microformats_types::temporal::Offset;
    ///
    /// assert_eq!(Offset::from_str("Z").map(|o| o.to_string()), Ok("Z".to_owned()));
    /// assert_eq!(Offset::from_str("+1:11").map(|o| o.to_string()), Ok("+01:11".to_owned()));
    /// assert_eq!(Offset::from_str("-1:11").map(|o| o.to_string()), Ok("-01:11".to_owned()));
    /// assert_eq!(Offset::from_str("-0330").map(|o| o.to_string()), Ok("-0330".to_owned()));
    /// ```
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let symbol_char = s.to_ascii_lowercase().chars().next();
        let (offset_secs, with_minutes, with_colon) = s
            .get(1..)
            .and_then(|offset| RE_OFFSET_RANGE.captures(offset))
            .map(|captures| {
                let hour: i32 = captures
                    .name("hour")
                    .and_then(|v| v.as_str().parse().ok())
                    .unwrap_or_default();
                let (minute, has_minutes): (i32, bool) = (
                    captures
                        .name("minute")
                        .and_then(|v| v.as_str().parse().ok())
                        .unwrap_or_default(),
                    captures.name("minute").is_some(),
                );
                let with_colon = captures
                    .name("offset_colon")
                    .filter(|m| !m.as_str().is_empty())
                    .is_some();

                (hour * 60 * 60 + minute * 60, has_minutes, with_colon)
            })
            .unwrap_or_default();

        if symbol_char == Some('z') {
            time::UtcOffset::from_whole_seconds(0)
                .map_err(|e| Self::Err::Time(e.into()))
                .map(|data| Self {
                    data,
                    with_minutes,
                    with_colon,
                })
        } else if matches!(symbol_char, Some('-')) || matches!(symbol_char, Some('+')) {
            time::UtcOffset::from_whole_seconds(
                offset_secs
                    * if matches!(symbol_char, Some('+')) {
                        1
                    } else {
                        -1
                    },
            )
            .map_err(|e| Self::Err::Time(e.into()))
            .map(|data| Self {
                data,
                with_minutes,
                with_colon,
            })
        } else {
            Err(Self::Err::InvalidOffset(s.to_string()))
        }
    }
}

impl Offset {
    fn from_offset(data: time::UtcOffset) -> Self {
        Self {
            data,
            with_minutes: true,
            with_colon: false,
        }
    }
}

impl From<time::UtcOffset> for Offset {
    fn from(value: time::UtcOffset) -> Self {
        Self::from_offset(value)
    }
}

#[test]
fn offset_to_string() {
    assert_eq!(
        Offset::from_str("-0800").map(|o| o.to_string()),
        Ok("-0800".to_owned())
    );
    assert_eq!(
        Offset::from_str("-08:00").map(|o| o.to_string()),
        Ok("-08:00".to_owned())
    );
    assert_eq!(
        Offset::from_str("-08").map(|o| o.to_string()),
        Ok("-08".to_owned())
    );
    assert_eq!(
        Offset::from_str("+00").map(|o| o.to_string()),
        Ok("Z".to_owned())
    );
    assert_eq!(
        Offset::from_str("+00:00").map(|o| o.to_string()),
        Ok("Z".to_owned())
    );
    assert_eq!(
        Offset::from_str("-00:00").map(|o| o.to_string()),
        Ok("Z".to_owned())
    );
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
    pub data: time::Time,
    pub offset: Option<Offset>,
    pub has_seconds: bool,
    pub prefix: Option<char>,
}

impl Time {
    pub fn with_offset(&self, offset: Option<Offset>) -> Self {
        let mut other = self.clone();
        other.offset = offset;
        other
    }

    pub fn from_time(
        data: time::Time,
        has_seconds: bool,
        prefix: Option<char>,
        offset: Option<Offset>,
    ) -> Self {
        Self {
            data,
            has_seconds,
            prefix,
            offset,
        }
    }
}

impl ToString for Time {
    fn to_string(&self) -> String {
        let mut format_items = vec![];
        if let Some(prefix) = self.prefix {
            format_items.push(prefix.to_string());
        }

        let hour = self.data.hour();
        let minute = self.data.minute();

        format_items.push(if self.has_seconds {
            let second = self.data.second();
            let subseconds = self.data.nanosecond();
            let seconds = if subseconds != 0 {
                format!("{second:02}.{subseconds:0>9}")
                    .trim_end_matches('0')
                    .to_string()
            } else {
                format!("{second:02}")
            };
            format!("{hour:02}:{minute:02}:{seconds}")
        } else {
            format!("{hour:02}:{minute:02}")
        });

        let time_string = format_items.join("");
        let offset_string = self.offset.as_ref().map(|o| o.to_string());

        [time_string, offset_string.unwrap_or_default()].join("")
    }
}

#[test]
fn time_to_string() {
    assert_eq!(
        Time::from_str("1 PM").map(|t| t.to_string()),
        Ok("13:00".to_owned())
    );
    assert_eq!(
        Time::from_str("1:30:10.302 PM").map(|t| t.to_string()),
        Ok("13:30:10.302".to_owned())
    );

    assert_eq!(
        Time::from_str("T1 PM").map(|t| t.to_string()),
        Ok("T13:00".to_owned())
    );
    assert_eq!(
        Time::from_str(" 1 PM").map(|t| t.to_string()),
        Ok(" 13:00".to_owned())
    );

    assert_eq!(
        Time::from_str("March 14th 2013").map(|t| t.to_string()),
        Err(Error::InvalidTime("March 14th 2013".to_string()))
    );
}

impl FromStr for Time {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        RE_TIME_HMS_OFFSET_MERIDAN
            .captures(s)
            .ok_or_else(|| Self::Err::InvalidTime(s.to_string()))
            .and_then(|params| {
                let has_seconds = params.name("second").is_some();
                let prefix = params
                    .name("prefix")
                    .and_then(|m| m.as_str().chars().next());
                let mut hour = params
                    .name("hour")
                    .filter(|s| !s.is_empty())
                    .map(|m| m.as_str())
                    .ok_or_else(|| Self::Err::MissingHour(s.to_string()))?
                    .parse()
                    .map_err(Self::Err::Integer)?;
                let min = params
                    .name("minute")
                    .filter(|s| !s.is_empty())
                    .map(|m| m.as_str())
                    .unwrap_or("0")
                    .parse()
                    .map_err(Self::Err::Integer)?;
                let second = params
                    .name("second")
                    .filter(|s| !s.is_empty())
                    .map(|m| m.as_str())
                    .unwrap_or("0")
                    .parse()
                    .map_err(Self::Err::Integer)?;
                let nano = params
                    .name("nano")
                    .filter(|s| !s.is_empty())
                    .map(|m| format!("{:0<9}", m.as_str().trim_start_matches('0')))
                    .unwrap_or_else(|| "0".to_string())
                    .parse()
                    .map_err(Self::Err::Integer)?;

                let offset = if let Some(offset_str) = params.name("tz") {
                    Some(Offset::from_str(&offset_str.as_str().replace(':', ""))?)
                } else {
                    None
                };

                if Some("pm".to_string())
                    == params
                        .name("meridan")
                        .map(|m| m.as_str().to_ascii_lowercase().replace('.', ""))
                    && hour < 12
                {
                    hour += 12;
                }

                time::Time::from_hms_nano(hour, min, second, nano)
                    .map(|data| Self {
                        data,
                        offset,
                        has_seconds,
                        prefix,
                    })
                    .map_err(|_| Self::Err::InvalidTime(s.to_owned()))
            })
    }
}

impl From<time::Time> for Time {
    fn from(data: time::Time) -> Self {
        Self {
            data,
            offset: None,
            prefix: None,
            has_seconds: false,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
    pub data: time::Date,
    pub ordinal: bool,
    pub has_day: bool,
}

impl Date {
    fn from_date(data: time::Date, ordinal: bool, has_day: bool) -> Self {
        Self {
            data,
            ordinal,
            has_day,
        }
    }
}

impl ToString for Date {
    fn to_string(&self) -> String {
        let year = self.data.year();
        if self.ordinal {
            let oridinal_day = self.data.ordinal();
            format!("{year:04}-{oridinal_day:03}")
        } else if !self.has_day {
            let month: u8 = self.data.month().into();
            format!("{year:04}-{month:02}")
        } else {
            let month: u8 = self.data.month().into();
            let day = self.data.day();
            format!("{year:04}-{month:02}-{day:02}")
        }
    }
}

impl FromStr for Date {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let result = if let Some(parts) = RE_DATE_YO.captures(s) {
            let year = parts
                .name("year")
                .and_then(|v| v.as_str().parse().ok())
                .ok_or_else(|| Error::MissingYear(s.to_string()))?;
            let ordinal = parts
                .name("ordinal")
                .and_then(|v| v.as_str().parse().ok())
                .ok_or_else(|| Error::MissingOrdinalDay(s.to_string()))?;

            time::Date::from_ordinal_date(year, ordinal)
                .map(|d| (d, false))
                .map_err(|e| Self::Err::Time(e.into()))
        } else if let Some(parts) = RE_DATE_YMD.captures(s) {
            parts
                .name("year")
                .ok_or_else(|| Error::MissingYear(s.to_string()))
                .or_else(|_| {
                    parts
                        .name("month")
                        .ok_or_else(|| Error::MissingMonth(s.to_string()))
                })?;

            let has_day = parts.name("day").is_some();
            let adjusted_s = if has_day {
                s.to_string()
            } else {
                format!("{}-01", s)
            };

            time::Date::parse(&adjusted_s, format_description!("[year]-[month]-[day]"))
                .map_err(|e| Self::Err::Time(e.into()))
                .map(|d| (d, has_day))
        } else if let Some(parts) = RE_DATE_YM.captures(s) {
            parts
                .name("year")
                .ok_or_else(|| Error::MissingYear(s.to_string()))
                .or_else(|_| {
                    parts
                        .name("month")
                        .ok_or_else(|| Error::MissingMonth(s.to_string()))
                })?;

            time::Date::parse(s, format_description!("[year]-[month]"))
                .map_err(|e| Self::Err::Time(e.into()))
                .map(|d| (d, false))
        } else {
            Err(Self::Err::InvalidDate(s.to_string()))
        };

        result.map(|(data, has_day)| Self {
            data,
            ordinal: RE_DATE_YO.is_match(s),
            has_day,
        })
    }
}

impl From<time::Date> for Date {
    fn from(data: time::Date) -> Self {
        Self {
            data,
            ordinal: false,
            has_day: true,
        }
    }
}

#[cfg(test)]
#[test]
fn date_to_string() {
    assert_eq!(
        Date::from_str("2013-034").map(|s| s.to_string()),
        Ok("2013-034".to_owned())
    );
    assert_eq!(
        Date::from_str("2013-03").map(|s| s.to_string()),
        Ok("2013-03".to_owned())
    );
    assert_eq!(
        Date::from_str("2013-03-01").map(|s| s.to_string()),
        Ok("2013-03-01".to_owned())
    );
}

#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct Stamp {
    pub time: Option<Time>,
    pub date: Option<Date>,
    pub offset: Option<Offset>,
    was_iso8601: bool,
}

impl std::fmt::Display for Stamp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let v = [
            self.date.as_ref().map(|s| s.to_string()),
            self.time
                .as_ref()
                .map(|t| {
                    if self.offset.is_some() {
                        t.with_offset(self.offset.clone())
                    } else {
                        t.clone()
                    }
                })
                .map(|t| t.to_string()),
        ]
        .iter()
        .filter_map(|o| o.clone())
        .collect::<Vec<_>>()
        .join("");

        f.write_str(&v)
    }
}

impl std::fmt::Debug for Stamp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Stamp")
            .field("value", &self.to_string())
            .field("was_iso8601", &self.was_iso8601)
            .finish()
    }
}

impl Stamp {
    pub fn from_iso8601(temporal_value: &str) -> Result<Self, Error> {
        if let Some(components) = RE_ISO8601_DATETIME_OFFSET.captures(temporal_value) {
            let prefix = components
                .name("prefix")
                .map(|p| p.as_str())
                .and_then(|c| c.chars().next());
            time::OffsetDateTime::parse(
                temporal_value,
                &time::format_description::well_known::Rfc3339,
            )
            .map_err(|e| Error::Time(e.into()))
            .map(|dt| Self {
                date: Some(Date::from_date(dt.date(), false, true)),
                time: Some(Time::from_time(dt.time(), true, prefix, None)).map(|mut t| {
                    t.prefix = prefix;
                    t
                }),
                offset: Some(Offset::from_offset(dt.offset())),
                was_iso8601: true,
            })
            .or_else(|_| {
                let mut parts = temporal_value
                    .splitn(2, prefix.unwrap_or('T'))
                    .map(|s| s.to_owned());

                let date_str = parts.next().unwrap();
                let date = Date::from_str(&date_str)?;
                let time_str = parts.next().unwrap();
                let time = Time::from_str(&time_str).map(|mut t| {
                    t.prefix = prefix;
                    t
                })?;

                Ok(Stamp::compose(Some(date), Some(time), None))
            })
        } else {
            Err(Error::NonISO8601DateTime(temporal_value.to_string()))
        }
    }

    pub fn from_rfc2822(temporal_value: &str) -> Result<Self, Error> {
        time::OffsetDateTime::parse(
            temporal_value,
            &time::format_description::well_known::Rfc2822,
        )
        .map(|dt| Self {
            time: Some(dt.time().into()),
            date: Some(dt.date().into()),
            offset: Some(dt.offset().into()),
            was_iso8601: true,
        })
        .map_err(|_| Error::NonRfc2822DateTime(temporal_value.to_string()))
    }

    /// Converts a string into a timestamp, offset, date or time value.
    ///
    /// * `temporal_value`: A string-friendly value.
    pub fn parse(temporal_value: impl Into<String>) -> Result<Self, Error> {
        let temporal_str_value = temporal_value.into();
        let santizied_temporal_value = temporal_str_value.trim().to_string();

        if let Ok(dt) = Self::from_rfc2822(&santizied_temporal_value) {
            return Ok(dt);
        }

        let values = santizied_temporal_value
            .split(' ')
            .map(|t| t.to_string())
            .collect::<Vec<_>>();

        let iso = Self::from_iso8601(&santizied_temporal_value);

        // Match against the whole string.
        if iso.is_ok() {
            iso
        } else if RE_TIME_OFFSET.is_match(&santizied_temporal_value) {
            Offset::from_str(&santizied_temporal_value).map(|offset| offset.into())
        } else if RE_DATE_YO.is_match(&santizied_temporal_value)
            || RE_DATE_YMD.is_match(&santizied_temporal_value)
        {
            Date::from_str(&santizied_temporal_value).map(|date| date.into())
        } else if RE_TIME_HMS_OFFSET_MERIDAN.is_match(&santizied_temporal_value) {
            Time::from_str(&santizied_temporal_value).map(|time| time.into())
        } else if values.len() != 1 {
            let mut date = None;
            let mut time = None;
            let mut offset = None;

            let parsed_values = values.iter().cloned().flat_map(|v| {
                temporal::Stamp::parse(&v)
                    .or_else(|_| temporal::Stamp::from_rfc2822(&v))
                    .ok()
            });

            if let Some(stamp) = parsed_values.clone().find(|v| v.is_stamp()) {
                return Ok(stamp);
            } else {
                for value in parsed_values {
                    if value.is_date() && date.is_none() {
                        date = value.as_date()
                    } else if value.is_time() && time.is_none() {
                        time = value.as_time();
                    } else if value.is_offset() && offset.is_none() {
                        offset = value.as_offset();
                    }
                }

                time = time.map(|mut t| {
                    if t.prefix.is_none() {
                        t.prefix = Some(' ');
                    }
                    t
                });

                Ok(temporal::Stamp::compose(date, time, offset))
            }
        } else {
            Err(Error::CompletelyInvalid(
                santizied_temporal_value.to_string(),
            ))
        }
    }

    pub fn is_date(&self) -> bool {
        self.date.is_some() && self.time.is_none() && self.offset.is_none()
    }
    pub fn is_time(&self) -> bool {
        self.date.is_none() && self.time.is_some()
    }
    pub fn is_offset(&self) -> bool {
        self.date.is_none() && self.time.is_none() && self.offset.is_some()
    }

    pub fn as_date(&self) -> Option<Date> {
        self.date.clone()
    }
    pub fn as_time(&self) -> Option<Time> {
        self.time.clone()
    }
    pub fn as_offset(&self) -> Option<Offset> {
        self.offset.clone()
    }

    /// Merges a date, time and offset conditionally into a singluar datetime with offset.
    pub fn compose(date: Option<Date>, time: Option<Time>, offset: Option<Offset>) -> Self {
        Self {
            date,
            time,
            offset,
            was_iso8601: false,
        }
    }

    /// Generates a [stamp][Stamp] from the current time in UTC.
    pub fn now() -> Self {
        time::OffsetDateTime::now_utc().into()
    }

    pub fn is_stamp(&self) -> bool {
        self.date.is_some() && self.time.is_some()
    }

    pub fn is_empty(&self) -> bool {
        self.date.is_none() && self.time.is_none() && self.offset.is_none()
    }
}

impl std::default::Default for Stamp {
    fn default() -> Self {
        Self::now()
    }
}

impl FromStr for Stamp {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl From<Date> for Stamp {
    fn from(value: Date) -> Self {
        Self {
            date: Some(value),
            time: None,
            offset: None,
            was_iso8601: false,
        }
    }
}

impl From<Time> for Stamp {
    fn from(value: Time) -> Self {
        Self {
            time: Some(value),
            date: None,
            offset: None,
            was_iso8601: false,
        }
    }
}

impl From<Offset> for Stamp {
    fn from(value: Offset) -> Self {
        Self {
            offset: Some(value),
            date: None,
            time: None,
            was_iso8601: false,
        }
    }
}

impl From<time::OffsetDateTime> for Stamp {
    fn from(dt: time::OffsetDateTime) -> Self {
        Self {
            time: Some(Time::from_time(dt.time(), true, Some('T'), None)),
            date: Some(Date::from_date(dt.date(), false, true)),
            offset: Some(Offset::from_offset(dt.offset())),
            was_iso8601: true,
        }
    }
}

impl serde::Serialize for Stamp {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let stamp_string = self.to_string();
        stamp_string.serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for Stamp {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        String::deserialize(deserializer).and_then(|stamp_string| {
            Self::from_str(&stamp_string).map_err(serde::de::Error::custom)
        })
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Duration {
    pub year: Option<u32>,
    pub month: Option<u32>,
    pub week: Option<u32>,
    pub day: Option<u32>,
    pub time: Option<Time>,
}

impl FromStr for Duration {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some(parts) = RE_DURATION.captures(s) {
            let year = parts.name("year").and_then(|v| v.as_str().parse().ok());
            let month = parts.name("month").and_then(|v| v.as_str().parse().ok());
            let week = parts.name("week").and_then(|v| v.as_str().parse().ok());
            let day = parts.name("day").and_then(|v| v.as_str().parse().ok());

            let time = if parts.name("time").is_some() {
                let hour = parts
                    .name("hour")
                    .and_then(|v| v.as_str().parse().ok())
                    .unwrap_or(0);
                let minute = parts
                    .name("minute")
                    .and_then(|v| v.as_str().parse().ok())
                    .unwrap_or(0);
                let second = parts
                    .name("second")
                    .and_then(|v| v.as_str().parse().ok())
                    .unwrap_or(0);

                Some(Time::from_str(&format!("{}:{}:{}", hour, minute, second))?)
            } else {
                None
            };

            Ok(Self {
                year,
                month,
                week,
                day,
                time,
            })
        } else {
            Err(Error::CompletelyInvalid(s.to_string()))
        }
    }
}

impl ToString for Duration {
    fn to_string(&self) -> String {
        let mut period = vec![];
        let mut time = vec![];

        if let Some(y) = self.year {
            period.push(format!("{}Y", y));
        }

        if let Some(m) = self.month {
            period.push(format!("{}M", m));
        }

        if let Some(w) = self.week {
            period.push(format!("{}W", w));
        }

        if let Some(d) = self.day {
            period.push(format!("{}D", d));
        }

        if let Some(t) = &self.time {
            time.push(format!("{}H", t.data.hour()));
            time.push(format!("{}M", t.data.minute()));
            time.push(format!("{}S", t.data.second()));
        }

        format!("P{}{}", period.join(""), time.join(""))
    }
}

impl serde::Serialize for Duration {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.to_string().serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for Duration {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        String::deserialize(deserializer)
            .and_then(|s| Self::from_str(&s).map_err(|e| serde::de::Error::custom(Box::new(e))))
    }
}

// #[derive(Debug, Clone)]
// pub enum Interval {
//     Within(Stamp, Stamp),
//     After(Stamp, Duration),
//     Until(Duration, Stamp),
//     Periodic(Duration),
// }

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Value {
    Duration(Duration),
    Timestamp(Stamp),
}

impl ToString for Value {
    fn to_string(&self) -> String {
        match self {
            Self::Duration(d) => d.to_string(),
            Self::Timestamp(t) => t.to_string(),
        }
    }
}

impl FromStr for Value {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Duration::from_str(s)
            .map(Self::Duration)
            .or_else(|_| Stamp::from_str(s).map(Self::Timestamp))
    }
}

impl serde::Serialize for Value {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let value_string = self.to_string();
        value_string.serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for Value {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value_string = String::deserialize(deserializer)?;
        Self::from_str(&value_string).map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
#[test]
fn value_to_string() {
    assert_eq!(
        Value::from_str("P2Y").map(|s| s.to_string()),
        Ok("P2Y".to_string())
    );
}

#[cfg(test)]
#[test]
fn stamp_to_string() {
    assert_eq!(
        Stamp::from_str("Mon, 16 May 2022 20:41:45 GMT").map(|s| s.to_string()),
        Ok("2022-05-1620:41Z".to_owned()),
        "format from issue #7"
    );
    assert_eq!(
        Stamp::from_str("2000-10-01 1:00").map(|s| s.to_string()),
        Ok("2000-10-01 01:00".to_owned())
    );
    assert_eq!(
        Stamp::from_str("19:00:00-08:00").map(|s| s.to_string()),
        Ok("19:00:00-0800".to_owned())
    );
    assert_eq!(
        Stamp::from_str("2000-10-01 19:00:00-08:00").map(|s| s.to_string()),
        Ok("2000-10-01 19:00:00-0800".to_owned())
    );
    assert_eq!(
        Stamp::from_str("2000-10-01 19:00:00-0800").map(|s| s.to_string()),
        Ok("2000-10-01 19:00:00-0800".to_owned())
    );
    assert_eq!(
        Stamp::from_str("2009-06-26T19:00:00-08:00").map(|s| s.to_string()),
        Ok("2009-06-26T19:00:00-0800".to_owned())
    );
    assert_eq!(
        Stamp::from_str("2009-06-26T19:00-08:00").map(|s| s.to_string()),
        Ok("2009-06-26T19:00-0800".to_owned()),
        "keeping the colon out of the output"
    );
    assert_eq!(
        Stamp::from_str("2009-06-26T19:00-08").map(|s| s.to_string()),
        Ok("2009-06-26T19:00-08".to_owned()),
        "don't add the minutes in the offset"
    );
    assert_eq!(
        Stamp::from_str("2009-06-26T19:00-0800").map(|s| s.to_string()),
        Ok("2009-06-26T19:00-0800".to_owned()),
        "no colon in offset, looking ISO8601-y"
    );
    assert_eq!(
        Stamp::from_str("2009-06-26 19:00+0800").map(|s| s.to_string()),
        Ok("2009-06-26 19:00+0800".to_owned()),
        "maintain lack of colon in non-ISO8601 value"
    );
}
