//! Parsing implementations for all [`Component`](crate::format_description::Component)s.

use core::num::{NonZeroU16, NonZeroU8};

use num_conv::prelude::*;

use crate::convert::*;
use crate::format_description::modifier;
#[cfg(feature = "large-dates")]
use crate::parsing::combinator::n_to_m_digits_padded;
use crate::parsing::combinator::{
    any_digit, exactly_n_digits, exactly_n_digits_padded, first_match, n_to_m_digits, opt, sign,
};
use crate::parsing::ParsedItem;
use crate::{Month, Weekday};

// region: date components
/// Parse the "year" component of a `Date`.
pub(crate) fn parse_year(input: &[u8], modifiers: modifier::Year) -> Option<ParsedItem<'_, i32>> {
    match modifiers.repr {
        modifier::YearRepr::Full => {
            let ParsedItem(input, sign) = opt(sign)(input);
            #[cfg(not(feature = "large-dates"))]
            let ParsedItem(input, year) =
                exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
            #[cfg(feature = "large-dates")]
            let ParsedItem(input, year) =
                n_to_m_digits_padded::<4, 6, u32>(modifiers.padding)(input)?;
            match sign {
                Some(b'-') => Some(ParsedItem(input, -year.cast_signed())),
                None if modifiers.sign_is_mandatory || year >= 10_000 => None,
                _ => Some(ParsedItem(input, year.cast_signed())),
            }
        }
        modifier::YearRepr::LastTwo => Some(
            exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?.map(|v| v.cast_signed()),
        ),
    }
}

/// Parse the "month" component of a `Date`.
pub(crate) fn parse_month(
    input: &[u8],
    modifiers: modifier::Month,
) -> Option<ParsedItem<'_, Month>> {
    use Month::*;
    let ParsedItem(remaining, value) = first_match(
        match modifiers.repr {
            modifier::MonthRepr::Numerical => {
                return exactly_n_digits_padded::<2, _>(modifiers.padding)(input)?
                    .flat_map(|n| Month::from_number(n).ok());
            }
            modifier::MonthRepr::Long => [
                (b"January".as_slice(), January),
                (b"February".as_slice(), February),
                (b"March".as_slice(), March),
                (b"April".as_slice(), April),
                (b"May".as_slice(), May),
                (b"June".as_slice(), June),
                (b"July".as_slice(), July),
                (b"August".as_slice(), August),
                (b"September".as_slice(), September),
                (b"October".as_slice(), October),
                (b"November".as_slice(), November),
                (b"December".as_slice(), December),
            ],
            modifier::MonthRepr::Short => [
                (b"Jan".as_slice(), January),
                (b"Feb".as_slice(), February),
                (b"Mar".as_slice(), March),
                (b"Apr".as_slice(), April),
                (b"May".as_slice(), May),
                (b"Jun".as_slice(), June),
                (b"Jul".as_slice(), July),
                (b"Aug".as_slice(), August),
                (b"Sep".as_slice(), September),
                (b"Oct".as_slice(), October),
                (b"Nov".as_slice(), November),
                (b"Dec".as_slice(), December),
            ],
        },
        modifiers.case_sensitive,
    )(input)?;
    Some(ParsedItem(remaining, value))
}

/// Parse the "week number" component of a `Date`.
pub(crate) fn parse_week_number(
    input: &[u8],
    modifiers: modifier::WeekNumber,
) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
}

/// Parse the "weekday" component of a `Date`.
pub(crate) fn parse_weekday(
    input: &[u8],
    modifiers: modifier::Weekday,
) -> Option<ParsedItem<'_, Weekday>> {
    first_match(
        match (modifiers.repr, modifiers.one_indexed) {
            (modifier::WeekdayRepr::Short, _) => [
                (b"Mon".as_slice(), Weekday::Monday),
                (b"Tue".as_slice(), Weekday::Tuesday),
                (b"Wed".as_slice(), Weekday::Wednesday),
                (b"Thu".as_slice(), Weekday::Thursday),
                (b"Fri".as_slice(), Weekday::Friday),
                (b"Sat".as_slice(), Weekday::Saturday),
                (b"Sun".as_slice(), Weekday::Sunday),
            ],
            (modifier::WeekdayRepr::Long, _) => [
                (b"Monday".as_slice(), Weekday::Monday),
                (b"Tuesday".as_slice(), Weekday::Tuesday),
                (b"Wednesday".as_slice(), Weekday::Wednesday),
                (b"Thursday".as_slice(), Weekday::Thursday),
                (b"Friday".as_slice(), Weekday::Friday),
                (b"Saturday".as_slice(), Weekday::Saturday),
                (b"Sunday".as_slice(), Weekday::Sunday),
            ],
            (modifier::WeekdayRepr::Sunday, false) => [
                (b"1".as_slice(), Weekday::Monday),
                (b"2".as_slice(), Weekday::Tuesday),
                (b"3".as_slice(), Weekday::Wednesday),
                (b"4".as_slice(), Weekday::Thursday),
                (b"5".as_slice(), Weekday::Friday),
                (b"6".as_slice(), Weekday::Saturday),
                (b"0".as_slice(), Weekday::Sunday),
            ],
            (modifier::WeekdayRepr::Sunday, true) => [
                (b"2".as_slice(), Weekday::Monday),
                (b"3".as_slice(), Weekday::Tuesday),
                (b"4".as_slice(), Weekday::Wednesday),
                (b"5".as_slice(), Weekday::Thursday),
                (b"6".as_slice(), Weekday::Friday),
                (b"7".as_slice(), Weekday::Saturday),
                (b"1".as_slice(), Weekday::Sunday),
            ],
            (modifier::WeekdayRepr::Monday, false) => [
                (b"0".as_slice(), Weekday::Monday),
                (b"1".as_slice(), Weekday::Tuesday),
                (b"2".as_slice(), Weekday::Wednesday),
                (b"3".as_slice(), Weekday::Thursday),
                (b"4".as_slice(), Weekday::Friday),
                (b"5".as_slice(), Weekday::Saturday),
                (b"6".as_slice(), Weekday::Sunday),
            ],
            (modifier::WeekdayRepr::Monday, true) => [
                (b"1".as_slice(), Weekday::Monday),
                (b"2".as_slice(), Weekday::Tuesday),
                (b"3".as_slice(), Weekday::Wednesday),
                (b"4".as_slice(), Weekday::Thursday),
                (b"5".as_slice(), Weekday::Friday),
                (b"6".as_slice(), Weekday::Saturday),
                (b"7".as_slice(), Weekday::Sunday),
            ],
        },
        modifiers.case_sensitive,
    )(input)
}

/// Parse the "ordinal" component of a `Date`.
pub(crate) fn parse_ordinal(
    input: &[u8],
    modifiers: modifier::Ordinal,
) -> Option<ParsedItem<'_, NonZeroU16>> {
    exactly_n_digits_padded::<3, _>(modifiers.padding)(input)
}

/// Parse the "day" component of a `Date`.
pub(crate) fn parse_day(
    input: &[u8],
    modifiers: modifier::Day,
) -> Option<ParsedItem<'_, NonZeroU8>> {
    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
}
// endregion date components

// region: time components
/// Indicate whether the hour is "am" or "pm".
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Period {
    #[allow(clippy::missing_docs_in_private_items)]
    Am,
    #[allow(clippy::missing_docs_in_private_items)]
    Pm,
}

/// Parse the "hour" component of a `Time`.
pub(crate) fn parse_hour(input: &[u8], modifiers: modifier::Hour) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
}

/// Parse the "minute" component of a `Time`.
pub(crate) fn parse_minute(
    input: &[u8],
    modifiers: modifier::Minute,
) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
}

/// Parse the "second" component of a `Time`.
pub(crate) fn parse_second(
    input: &[u8],
    modifiers: modifier::Second,
) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
}

/// Parse the "period" component of a `Time`. Required if the hour is on a 12-hour clock.
pub(crate) fn parse_period(
    input: &[u8],
    modifiers: modifier::Period,
) -> Option<ParsedItem<'_, Period>> {
    first_match(
        if modifiers.is_uppercase {
            [
                (b"AM".as_slice(), Period::Am),
                (b"PM".as_slice(), Period::Pm),
            ]
        } else {
            [
                (b"am".as_slice(), Period::Am),
                (b"pm".as_slice(), Period::Pm),
            ]
        },
        modifiers.case_sensitive,
    )(input)
}

/// Parse the "subsecond" component of a `Time`.
pub(crate) fn parse_subsecond(
    input: &[u8],
    modifiers: modifier::Subsecond,
) -> Option<ParsedItem<'_, u32>> {
    use modifier::SubsecondDigits::*;
    Some(match modifiers.digits {
        One => exactly_n_digits::<1, u32>(input)?.map(|v| v * 100_000_000),
        Two => exactly_n_digits::<2, u32>(input)?.map(|v| v * 10_000_000),
        Three => exactly_n_digits::<3, u32>(input)?.map(|v| v * 1_000_000),
        Four => exactly_n_digits::<4, u32>(input)?.map(|v| v * 100_000),
        Five => exactly_n_digits::<5, u32>(input)?.map(|v| v * 10_000),
        Six => exactly_n_digits::<6, u32>(input)?.map(|v| v * 1_000),
        Seven => exactly_n_digits::<7, u32>(input)?.map(|v| v * 100),
        Eight => exactly_n_digits::<8, u32>(input)?.map(|v| v * 10),
        Nine => exactly_n_digits::<9, _>(input)?,
        OneOrMore => {
            let ParsedItem(mut input, mut value) =
                any_digit(input)?.map(|v| (v - b'0').extend::<u32>() * 100_000_000);

            let mut multiplier = 10_000_000;
            while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
                value += (digit - b'0').extend::<u32>() * multiplier;
                input = new_input;
                multiplier /= 10;
            }

            ParsedItem(input, value)
        }
    })
}
// endregion time components

// region: offset components
/// Parse the "hour" component of a `UtcOffset`.
///
/// Returns the value and whether the value is negative. This is used for when "-0" is parsed.
pub(crate) fn parse_offset_hour(
    input: &[u8],
    modifiers: modifier::OffsetHour,
) -> Option<ParsedItem<'_, (i8, bool)>> {
    let ParsedItem(input, sign) = opt(sign)(input);
    let ParsedItem(input, hour) = exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?;
    match sign {
        Some(b'-') => Some(ParsedItem(input, (-hour.cast_signed(), true))),
        None if modifiers.sign_is_mandatory => None,
        _ => Some(ParsedItem(input, (hour.cast_signed(), false))),
    }
}

/// Parse the "minute" component of a `UtcOffset`.
pub(crate) fn parse_offset_minute(
    input: &[u8],
    modifiers: modifier::OffsetMinute,
) -> Option<ParsedItem<'_, i8>> {
    Some(
        exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?
            .map(|offset_minute| offset_minute.cast_signed()),
    )
}

/// Parse the "second" component of a `UtcOffset`.
pub(crate) fn parse_offset_second(
    input: &[u8],
    modifiers: modifier::OffsetSecond,
) -> Option<ParsedItem<'_, i8>> {
    Some(
        exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?
            .map(|offset_second| offset_second.cast_signed()),
    )
}
// endregion offset components

/// Ignore the given number of bytes.
pub(crate) fn parse_ignore(
    input: &[u8],
    modifiers: modifier::Ignore,
) -> Option<ParsedItem<'_, ()>> {
    let modifier::Ignore { count } = modifiers;
    let input = input.get((count.get().extend())..)?;
    Some(ParsedItem(input, ()))
}

/// Parse the Unix timestamp component.
pub(crate) fn parse_unix_timestamp(
    input: &[u8],
    modifiers: modifier::UnixTimestamp,
) -> Option<ParsedItem<'_, i128>> {
    let ParsedItem(input, sign) = opt(sign)(input);
    let ParsedItem(input, nano_timestamp) = match modifiers.precision {
        modifier::UnixTimestampPrecision::Second => n_to_m_digits::<1, 14, u128>(input)?
            .map(|val| val * Nanosecond::per(Second).extend::<u128>()),
        modifier::UnixTimestampPrecision::Millisecond => n_to_m_digits::<1, 17, u128>(input)?
            .map(|val| val * Nanosecond::per(Millisecond).extend::<u128>()),
        modifier::UnixTimestampPrecision::Microsecond => n_to_m_digits::<1, 20, u128>(input)?
            .map(|val| val * Nanosecond::per(Microsecond).extend::<u128>()),
        modifier::UnixTimestampPrecision::Nanosecond => n_to_m_digits::<1, 23, _>(input)?,
    };

    match sign {
        Some(b'-') => Some(ParsedItem(input, -nano_timestamp.cast_signed())),
        None if modifiers.sign_is_mandatory => None,
        _ => Some(ParsedItem(input, nano_timestamp.cast_signed())),
    }
}

/// Parse the `end` component, which represents the end of input. If any input is remaining, `None`
/// is returned.
pub(crate) const fn parse_end(input: &[u8], end: modifier::End) -> Option<ParsedItem<'_, ()>> {
    let modifier::End {} = end;

    if input.is_empty() {
        Some(ParsedItem(input, ()))
    } else {
        None
    }
}
