use crate::{civil::Weekday, error, tz::Offset, util::escape};

#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Error {
    ColonCount {
        directive: u8,
    },
    DirectiveFailure {
        directive: u8,
        colons: u8,
    },
    DirectiveFailureDot {
        directive: u8,
    },
    ExpectedDirectiveAfterColons,
    ExpectedDirectiveAfterFlag {
        flag: u8,
    },
    ExpectedDirectiveAfterWidth,
    FailedStrftime,
    FailedStrptime,
    FailedWidth,
    InvalidDate,
    InvalidISOWeekDate,
    InvalidWeekdayMonday {
        got: Weekday,
    },
    InvalidWeekdaySunday {
        got: Weekday,
    },
    MismatchOffset {
        parsed: Offset,
        got: Offset,
    },
    MismatchWeekday {
        parsed: Weekday,
        got: Weekday,
    },
    MissingTimeHourForFractional,
    MissingTimeHourForMinute,
    MissingTimeHourForSecond,
    MissingTimeMinuteForFractional,
    MissingTimeMinuteForSecond,
    MissingTimeSecondForFractional,
    RangeTimestamp,
    RangeWidth,
    RequiredDateForDateTime,
    RequiredDateTimeForTimestamp,
    RequiredDateTimeForZoned,
    RequiredOffsetForTimestamp,
    RequiredSomeDayForDate,
    RequiredTimeForDateTime,
    RequiredYearForDate,
    UnconsumedStrptime {
        #[cfg(feature = "alloc")]
        remaining: alloc::boxed::Box<[u8]>,
    },
    UnexpectedEndAfterDot,
    UnexpectedEndAfterPercent,
    UnknownDirectiveAfterDot {
        directive: u8,
    },
    UnknownDirective {
        directive: u8,
    },
    ZonedOffsetOrTz,
}

impl Error {
    pub(crate) fn unconsumed(_remaining: &[u8]) -> Error {
        Error::UnconsumedStrptime {
            #[cfg(feature = "alloc")]
            remaining: _remaining.into(),
        }
    }
}

impl error::IntoError for Error {
    fn into_error(self) -> error::Error {
        self.into()
    }
}

impl From<Error> for error::Error {
    #[cold]
    #[inline(never)]
    fn from(err: Error) -> error::Error {
        error::ErrorKind::FmtStrtime(err).into()
    }
}

impl core::fmt::Display for Error {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        use self::Error::*;

        match *self {
            ColonCount { directive } => write!(
                f,
                "invalid number of `:` in `%{directive}` directive",
                directive = escape::Byte(directive),
            ),
            DirectiveFailure { directive, colons } => write!(
                f,
                "%{colons}{directive} failed",
                colons = escape::RepeatByte { byte: b':', count: colons },
                directive = escape::Byte(directive),
            ),
            DirectiveFailureDot { directive } => write!(
                f,
                "%.{directive} failed",
                directive = escape::Byte(directive),
            ),
            ExpectedDirectiveAfterColons => f.write_str(
                "expected to find specifier directive after colons, \
                 but found end of format string",
            ),
            ExpectedDirectiveAfterFlag { flag } => write!(
                f,
                "expected to find specifier directive after flag \
                 `{flag}`, but found end of format string",
                flag = escape::Byte(flag),
            ),
            ExpectedDirectiveAfterWidth => f.write_str(
                "expected to find specifier directive after parsed width, \
                 but found end of format string",
            ),
            FailedStrftime => f.write_str("strftime formatting failed"),
            FailedStrptime => f.write_str("strptime parsing failed"),
            FailedWidth => {
                f.write_str("failed to parse conversion specifier width")
            }
            InvalidDate => f.write_str("invalid date"),
            InvalidISOWeekDate => f.write_str("invalid ISO 8601 week date"),
            InvalidWeekdayMonday { got } => write!(
                f,
                "weekday `{got:?}` is not valid for \
                 Monday based week number",
            ),
            InvalidWeekdaySunday { got } => write!(
                f,
                "weekday `{got:?}` is not valid for \
                 Sunday based week number",
            ),
            MissingTimeHourForFractional => f.write_str(
                "parsing format did not include hour directive, \
                 but did include fractional second directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MissingTimeHourForMinute => f.write_str(
                "parsing format did not include hour directive, \
                 but did include minute directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MissingTimeHourForSecond => f.write_str(
                "parsing format did not include hour directive, \
                 but did include second directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MissingTimeMinuteForFractional => f.write_str(
                "parsing format did not include minute directive, \
                 but did include fractional second directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MissingTimeMinuteForSecond => f.write_str(
                "parsing format did not include minute directive, \
                 but did include second directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MissingTimeSecondForFractional => f.write_str(
                "parsing format did not include second directive, \
                 but did include fractional second directive (cannot have \
                 smaller time units with bigger time units missing)",
            ),
            MismatchOffset { parsed, got } => write!(
                f,
                "parsed time zone offset `{parsed}`, but \
                 offset from timestamp and time zone is `{got}`",
            ),
            MismatchWeekday { parsed, got } => write!(
                f,
                "parsed weekday `{parsed:?}` does not match \
                 weekday `{got:?}` from parsed date",
            ),
            RangeTimestamp => f.write_str(
                "parsed datetime and offset, \
                 but combining them into a zoned datetime \
                 is outside Jiff's supported timestamp range",
            ),
            RangeWidth => write!(
                f,
                "parsed width is too big, max is {max}",
                max = u8::MAX
            ),
            RequiredDateForDateTime => {
                f.write_str("date required to parse datetime")
            }
            RequiredDateTimeForTimestamp => {
                f.write_str("datetime required to parse timestamp")
            }
            RequiredDateTimeForZoned => {
                f.write_str("datetime required to parse zoned datetime")
            }
            RequiredOffsetForTimestamp => {
                f.write_str("offset required to parse timestamp")
            }
            RequiredSomeDayForDate => f.write_str(
                "a month/day, day-of-year or week date must be \
                 present to create a date, but none were found",
            ),
            RequiredTimeForDateTime => {
                f.write_str("time required to parse datetime")
            }
            RequiredYearForDate => f.write_str("year required to parse date"),
            #[cfg(feature = "alloc")]
            UnconsumedStrptime { ref remaining } => write!(
                f,
                "strptime expects to consume the entire input, but \
                 `{remaining}` remains unparsed",
                remaining = escape::Bytes(remaining),
            ),
            #[cfg(not(feature = "alloc"))]
            UnconsumedStrptime {} => f.write_str(
                "strptime expects to consume the entire input, but \
                 there is unparsed input remaining",
            ),
            UnexpectedEndAfterDot => f.write_str(
                "invalid format string, expected directive after `%.`",
            ),
            UnexpectedEndAfterPercent => f.write_str(
                "invalid format string, expected byte after `%`, \
                 but found end of format string",
            ),
            UnknownDirective { directive } => write!(
                f,
                "found unrecognized specifier directive `{directive}`",
                directive = escape::Byte(directive),
            ),
            UnknownDirectiveAfterDot { directive } => write!(
                f,
                "found unrecognized specifier directive `{directive}` \
                 following `%.`",
                directive = escape::Byte(directive),
            ),
            ZonedOffsetOrTz => f.write_str(
                "either offset (from `%z`) or IANA time zone identifier \
                 (from `%Q`) is required for parsing zoned datetime",
            ),
        }
    }
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum ParseError {
    ExpectedAmPm,
    ExpectedAmPmTooShort,
    ExpectedIanaTz,
    ExpectedIanaTzEndOfInput,
    ExpectedMonthAbbreviation,
    ExpectedMonthAbbreviationTooShort,
    ExpectedWeekdayAbbreviation,
    ExpectedWeekdayAbbreviationTooShort,
    ExpectedChoice {
        available: &'static [&'static [u8]],
    },
    ExpectedFractionalDigit,
    ExpectedMatchLiteralByte {
        expected: u8,
        got: u8,
    },
    ExpectedMatchLiteralEndOfInput {
        expected: u8,
    },
    ExpectedNonEmpty {
        directive: u8,
    },
    #[cfg(not(feature = "alloc"))]
    NotAllowedAlloc {
        directive: u8,
        colons: u8,
    },
    NotAllowedLocaleClockTime,
    NotAllowedLocaleDate,
    NotAllowedLocaleDateAndTime,
    NotAllowedLocaleTwelveHourClockTime,
    NotAllowedTimeZoneAbbreviation,
    ParseDay,
    ParseDayOfYear,
    ParseCentury,
    ParseFractionalSeconds,
    ParseHour,
    ParseIsoWeekNumber,
    ParseIsoWeekYear,
    ParseIsoWeekYearTwoDigit,
    ParseMinute,
    ParseMondayWeekNumber,
    ParseMonth,
    ParseSecond,
    ParseSundayWeekNumber,
    ParseTimestamp,
    ParseWeekdayNumber,
    ParseYear,
    ParseYearTwoDigit,
    UnknownMonthName,
    UnknownWeekdayAbbreviation,
}

impl error::IntoError for ParseError {
    fn into_error(self) -> error::Error {
        self.into()
    }
}

impl From<ParseError> for error::Error {
    #[cold]
    #[inline(never)]
    fn from(err: ParseError) -> error::Error {
        error::ErrorKind::FmtStrtimeParse(err).into()
    }
}

impl core::fmt::Display for ParseError {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        use self::ParseError::*;

        match *self {
            ExpectedAmPm => f.write_str("expected to find `AM` or `PM`"),
            ExpectedAmPmTooShort => f.write_str(
                "expected to find `AM` or `PM`, \
                 but the remaining input is too short \
                 to contain one",
            ),
            ExpectedIanaTz => f.write_str(
                "expected to find the start of an IANA time zone \
                 identifier name or component",
            ),
            ExpectedIanaTzEndOfInput => f.write_str(
                "expected to find the start of an IANA time zone \
                 identifier name or component, \
                 but found end of input instead",
            ),
            ExpectedMonthAbbreviation => {
                f.write_str("expected to find month name abbreviation")
            }
            ExpectedMonthAbbreviationTooShort => f.write_str(
                "expected to find month name abbreviation, \
                 but the remaining input is too short \
                 to contain one",
            ),
            ExpectedWeekdayAbbreviation => {
                f.write_str("expected to find weekday abbreviation")
            }
            ExpectedWeekdayAbbreviationTooShort => f.write_str(
                "expected to find weekday abbreviation, \
                 but the remaining input is too short \
                 to contain one",
            ),
            ExpectedChoice { available } => {
                f.write_str(
                    "failed to find expected value, available choices are: ",
                )?;
                for (i, choice) in available.iter().enumerate() {
                    if i > 0 {
                        f.write_str(", ")?;
                    }
                    write!(f, "{}", escape::Bytes(choice))?;
                }
                Ok(())
            }
            ExpectedFractionalDigit => f.write_str(
                "expected at least one fractional decimal digit, \
                 but did not find any",
            ),
            ExpectedMatchLiteralByte { expected, got } => write!(
                f,
                "expected to match literal byte `{expected}` from \
                 format string, but found byte `{got}` in input",
                expected = escape::Byte(expected),
                got = escape::Byte(got),
            ),
            ExpectedMatchLiteralEndOfInput { expected } => write!(
                f,
                "expected to match literal byte `{expected}` from \
                 format string, but found end of input",
                expected = escape::Byte(expected),
            ),
            ExpectedNonEmpty { directive } => write!(
                f,
                "expected non-empty input for directive `%{directive}`, \
                 but found end of input",
                directive = escape::Byte(directive),
            ),
            #[cfg(not(feature = "alloc"))]
            NotAllowedAlloc { directive, colons } => write!(
                f,
                "cannot parse `%{colons}{directive}` \
                 without Jiff's `alloc` feature enabled",
                colons = escape::RepeatByte { byte: b':', count: colons },
                directive = escape::Byte(directive),
            ),
            NotAllowedLocaleClockTime => {
                f.write_str("parsing locale clock time is not allowed")
            }
            NotAllowedLocaleDate => {
                f.write_str("parsing locale date is not allowed")
            }
            NotAllowedLocaleDateAndTime => {
                f.write_str("parsing locale date and time is not allowed")
            }
            NotAllowedLocaleTwelveHourClockTime => {
                f.write_str("parsing locale 12-hour clock time is not allowed")
            }
            NotAllowedTimeZoneAbbreviation => {
                f.write_str("parsing time zone abbreviation is not allowed")
            }
            ParseCentury => {
                f.write_str("failed to parse year number for century")
            }
            ParseDay => f.write_str("failed to parse day number"),
            ParseDayOfYear => {
                f.write_str("failed to parse day of year number")
            }
            ParseFractionalSeconds => f.write_str(
                "failed to parse fractional second component \
                 (up to 9 digits, nanosecond precision)",
            ),
            ParseHour => f.write_str("failed to parse hour number"),
            ParseMinute => f.write_str("failed to parse minute number"),
            ParseWeekdayNumber => {
                f.write_str("failed to parse weekday number")
            }
            ParseIsoWeekNumber => {
                f.write_str("failed to parse ISO 8601 week number")
            }
            ParseIsoWeekYear => {
                f.write_str("failed to parse ISO 8601 week year")
            }
            ParseIsoWeekYearTwoDigit => {
                f.write_str("failed to parse 2-digit ISO 8601 week year")
            }
            ParseMondayWeekNumber => {
                f.write_str("failed to parse Monday-based week number")
            }
            ParseMonth => f.write_str("failed to parse month number"),
            ParseSecond => f.write_str("failed to parse second number"),
            ParseSundayWeekNumber => {
                f.write_str("failed to parse Sunday-based week number")
            }
            ParseTimestamp => {
                f.write_str("failed to parse Unix timestamp (in seconds)")
            }
            ParseYear => f.write_str("failed to parse year"),
            ParseYearTwoDigit => f.write_str("failed to parse 2-digit year"),
            UnknownMonthName => f.write_str("unrecognized month name"),
            UnknownWeekdayAbbreviation => {
                f.write_str("unrecognized weekday abbreviation")
            }
        }
    }
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum FormatError {
    RequiresDate,
    RequiresInstant,
    RequiresOffset,
    RequiresTime,
    RequiresTimeZone,
    RequiresTimeZoneOrOffset,
    InvalidUtf8,
    ZeroPrecisionFloat,
    ZeroPrecisionNano,
}

impl error::IntoError for FormatError {
    fn into_error(self) -> error::Error {
        self.into()
    }
}

impl From<FormatError> for error::Error {
    #[cold]
    #[inline(never)]
    fn from(err: FormatError) -> error::Error {
        error::ErrorKind::FmtStrtimeFormat(err).into()
    }
}

impl core::fmt::Display for FormatError {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        use self::FormatError::*;

        match *self {
            RequiresDate => f.write_str("requires date to format"),
            RequiresInstant => f.write_str(
                "requires instant (a timestamp or a date, time and offset)",
            ),
            RequiresTime => f.write_str("requires time to format"),
            RequiresOffset => f.write_str("requires time zone offset"),
            RequiresTimeZone => {
                f.write_str("requires IANA time zone identifier")
            }
            RequiresTimeZoneOrOffset => f.write_str(
                "requires IANA time zone identifier or \
                 time zone offset, but neither were present",
            ),
            InvalidUtf8 => {
                f.write_str("invalid format string, it must be valid UTF-8")
            }
            ZeroPrecisionFloat => {
                f.write_str("zero precision with %f is not allowed")
            }
            ZeroPrecisionNano => {
                f.write_str("zero precision with %N is not allowed")
            }
        }
    }
}
