use std::iter::Peekable;

use proc_macro::{token_stream, TokenStream};

use crate::helpers::{
    consume_any_ident, consume_number, consume_punct, days_in_year, days_in_year_month,
    weeks_in_year, ymd_to_yo, ywd_to_yo,
};
use crate::to_tokens::ToTokens;
use crate::Error;

#[cfg(feature = "large-dates")]
const MAX_YEAR: i32 = 999_999;
#[cfg(not(feature = "large-dates"))]
const MAX_YEAR: i32 = 9_999;

pub(crate) struct Date {
    pub(crate) year: i32,
    pub(crate) ordinal: u16,
}

impl Date {
    pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<Self, Error> {
        let (year_sign_span, year_sign, explicit_sign) = if let Ok(span) = consume_punct('-', chars)
        {
            (Some(span), -1, true)
        } else if let Ok(span) = consume_punct('+', chars) {
            (Some(span), 1, true)
        } else {
            (None, 1, false)
        };
        let (year_span, mut year) = consume_number::<i32>("year", chars)?;
        year *= year_sign;
        if year.abs() > MAX_YEAR {
            return Err(Error::InvalidComponent {
                name: "year",
                value: year.to_string(),
                span_start: Some(year_sign_span.unwrap_or(year_span)),
                span_end: Some(year_span),
            });
        }
        if !explicit_sign && year.abs() >= 10_000 {
            return Err(Error::Custom {
                message: "years with more than four digits must have an explicit sign".into(),
                span_start: Some(year_sign_span.unwrap_or(year_span)),
                span_end: Some(year_span),
            });
        }

        consume_punct('-', chars)?;

        // year-week-day
        if let Ok(w_span) = consume_any_ident(&["W"], chars) {
            let (week_span, week) = consume_number::<u8>("week", chars)?;
            consume_punct('-', chars)?;
            let (day_span, day) = consume_number::<u8>("day", chars)?;

            if week > weeks_in_year(year) {
                return Err(Error::InvalidComponent {
                    name: "week",
                    value: week.to_string(),
                    span_start: Some(w_span),
                    span_end: Some(week_span),
                });
            }
            if day == 0 || day > 7 {
                return Err(Error::InvalidComponent {
                    name: "day",
                    value: day.to_string(),
                    span_start: Some(day_span),
                    span_end: Some(day_span),
                });
            }

            let (year, ordinal) = ywd_to_yo(year, week, day);

            return Ok(Self { year, ordinal });
        }

        // We don't yet know whether it's year-month-day or year-ordinal.
        let (month_or_ordinal_span, month_or_ordinal) =
            consume_number::<u16>("month or ordinal", chars)?;

        // year-month-day
        #[allow(clippy::branches_sharing_code)] // clarity
        if consume_punct('-', chars).is_ok() {
            let (month_span, month) = (month_or_ordinal_span, month_or_ordinal);
            let (day_span, day) = consume_number::<u8>("day", chars)?;

            if month == 0 || month > 12 {
                return Err(Error::InvalidComponent {
                    name: "month",
                    value: month.to_string(),
                    span_start: Some(month_span),
                    span_end: Some(month_span),
                });
            }
            let month = month as _;
            if day == 0 || day > days_in_year_month(year, month) {
                return Err(Error::InvalidComponent {
                    name: "day",
                    value: day.to_string(),
                    span_start: Some(day_span),
                    span_end: Some(day_span),
                });
            }

            let (year, ordinal) = ymd_to_yo(year, month, day);

            Ok(Self { year, ordinal })
        }
        // year-ordinal
        else {
            let (ordinal_span, ordinal) = (month_or_ordinal_span, month_or_ordinal);

            if ordinal == 0 || ordinal > days_in_year(year) {
                return Err(Error::InvalidComponent {
                    name: "ordinal",
                    value: ordinal.to_string(),
                    span_start: Some(ordinal_span),
                    span_end: Some(ordinal_span),
                });
            }

            Ok(Self { year, ordinal })
        }
    }
}

impl ToTokens for Date {
    fn into_token_stream(self) -> TokenStream {
        quote! {{
            const DATE: ::time::Date = ::time::Date::__from_ordinal_date_unchecked(
                #(self.year),
                #(self.ordinal),
            );
            DATE
        }}
    }
}
