use std::cmp::Ordering;

use std::fmt::Debug;
use std::hash::Hash;
use std::ops::{Add, Sub};

use ecow::{eco_format, EcoString, EcoVec};
use time::error::{Format, InvalidFormatDescription};
use time::macros::format_description;
use time::{format_description, Month, PrimitiveDateTime};

use super::{cast, func, scope, ty, Dict, Duration, Repr, Str, Value, Vm};
use crate::diag::{bail, StrResult};
use crate::geom::Smart;
use crate::util::pretty_array_like;
use crate::World;

/// Represents a date, a time, or a combination of both.
///
/// Can be created by either specifying a custom datetime using this type's
/// constructor function or getting the current date with
/// [`datetime.today`]($datetime.today).
///
/// # Example
/// ```example
/// #let date = datetime(
///   year: 2020,
///   month: 10,
///   day: 4,
/// )
///
/// #date.display() \
/// #date.display(
///   "y:[year repr:last_two]"
/// )
///
/// #let time = datetime(
///   hour: 18,
///   minute: 2,
///   second: 23,
/// )
///
/// #time.display() \
/// #time.display(
///   "h:[hour repr:12][period]"
/// )
/// ```
///
/// # Format
/// You can specify a customized formatting using the
/// [`display`]($datetime.display) method. The format of a datetime is
/// specified by providing _components_ with a specified number of _modifiers_.
/// A component represents a certain part of the datetime that you want to
/// display, and with the help of modifiers you can define how you want to
/// display that component. In order to display a component, you wrap the name
/// of the component in square brackets (e.g. `[[year]]` will display the year).
/// In order to add modifiers, you add a space after the component name followed
/// by the name of the modifier, a colon and the value of the modifier (e.g.
/// `[[month repr:short]]` will display the short representation of the month).
///
/// The possible combination of components and their respective modifiers is as
/// follows:
///
/// - `year`: Displays the year of the datetime.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     year is padded.
///   - `repr` Can be either `full` in which case the full year is displayed or
///     `last_two` in which case only the last two digits are displayed.
///   - `sign`: Can be either `automatic` or `mandatory`. Specifies when the
///     sign should be displayed.
/// - `month`: Displays the month of the datetime.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     month is padded.
///   - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the
///     month should be displayed as a number or a word. Unfortunately, when
///     choosing the word representation, it can currently only display the
///     English version. In the future, it is planned to support localization.
/// - `day`: Displays the day of the datetime.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     day is padded.
/// - `week_number`: Displays the week number of the datetime.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     week number is padded.
///   - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`,
///      week numbers are between 1 and 53, while the other ones are between 0
///      and 53.
/// - `weekday`: Displays the weekday of the date.
///   - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case
///     of `long` and `short`, the corresponding English name will be displayed
///     (same as for the month, other languages are currently not supported). In
///     the case of `sunday` and `monday`, the numerical value will be displayed
///     (assuming Sunday and Monday as the first day of the week, respectively).
///   - `one_indexed`: Can be either `true` or `false`. Defines whether the
///     numerical representation of the week starts with 0 or 1.
/// - `hour`: Displays the hour of the date.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     hour is padded.
///   - `repr`: Can be either `24` or `12`. Changes whether the hour is
///     displayed in the 24-hour or 12-hour format.
/// - `period`: The AM/PM part of the hour
///   - `case`: Can be `lower` to display it in lower case and `upper` to
///     display it in upper case.
/// - `minute`: Displays the minute of the date.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     minute is padded.
/// - `second`: Displays the second of the date.
///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
///     second is padded.
///
/// Keep in mind that not always all components can be used. For example, if you
/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it
/// will be stored as a plain date internally, meaning that you cannot use
/// components such as `hour` or `minute`, which would only work on datetimes
/// that have a specified time.
#[ty(scope)]
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum Datetime {
    /// Representation as a date.
    Date(time::Date),
    /// Representation as a time.
    Time(time::Time),
    /// Representation as a combination of date and time.
    Datetime(time::PrimitiveDateTime),
}

impl Datetime {
    /// Create a datetime from year, month, and day.
    pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
        Some(Datetime::Date(
            time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
                .ok()?,
        ))
    }

    /// Create a datetime from hour, minute, and second.
    pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
        Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
    }

    /// Create a datetime from day and time.
    pub fn from_ymd_hms(
        year: i32,
        month: u8,
        day: u8,
        hour: u8,
        minute: u8,
        second: u8,
    ) -> Option<Self> {
        let date =
            time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
                .ok()?;
        let time = time::Time::from_hms(hour, minute, second).ok()?;
        Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
    }

    /// Try to parse a dictionary as a TOML date.
    pub fn from_toml_dict(dict: &Dict) -> Option<Self> {
        if dict.len() != 1 {
            return None;
        }

        let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else {
            return None;
        };

        if let Ok(d) = time::PrimitiveDateTime::parse(
            string,
            &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
        ) {
            Self::from_ymd_hms(
                d.year(),
                d.month() as u8,
                d.day(),
                d.hour(),
                d.minute(),
                d.second(),
            )
        } else if let Ok(d) = time::PrimitiveDateTime::parse(
            string,
            &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
        ) {
            Self::from_ymd_hms(
                d.year(),
                d.month() as u8,
                d.day(),
                d.hour(),
                d.minute(),
                d.second(),
            )
        } else if let Ok(d) =
            time::Date::parse(string, &format_description!("[year]-[month]-[day]"))
        {
            Self::from_ymd(d.year(), d.month() as u8, d.day())
        } else if let Ok(d) =
            time::Time::parse(string, &format_description!("[hour]:[minute]:[second]"))
        {
            Self::from_hms(d.hour(), d.minute(), d.second())
        } else {
            None
        }
    }

    /// Which kind of variant this datetime stores.
    pub fn kind(&self) -> &'static str {
        match self {
            Datetime::Datetime(_) => "datetime",
            Datetime::Date(_) => "date",
            Datetime::Time(_) => "time",
        }
    }
}

#[scope]
impl Datetime {
    /// Creates a new datetime.
    ///
    /// You can specify the [datetime]($datetime) using a year, month, day,
    /// hour, minute, and second.
    ///
    /// _Note_: Depending on which components of the datetime you specify, Typst
    /// will store it in one of the following three ways:
    /// * If you specify year, month and day, Typst will store just a date.
    /// * If you specify hour, minute and second, Typst will store just a time.
    /// * If you specify all of year, month, day, hour, minute and second, Typst
    ///   will store a full datetime.
    ///
    /// Depending on how it is stored, the [`display`]($datetime.display) method
    /// will choose a different formatting by default.
    ///
    /// ```example
    /// #datetime(
    ///   year: 2012,
    ///   month: 8,
    ///   day: 3,
    /// ).display()
    /// ```
    #[func(constructor)]
    pub fn construct(
        /// The year of the datetime.
        #[named]
        year: Option<YearComponent>,
        /// The month of the datetime.
        #[named]
        month: Option<MonthComponent>,
        /// The day of the datetime.
        #[named]
        day: Option<DayComponent>,
        /// The hour of the datetime.
        #[named]
        hour: Option<HourComponent>,
        /// The minute of the datetime.
        #[named]
        minute: Option<MinuteComponent>,
        /// The second of the datetime.
        #[named]
        second: Option<SecondComponent>,
    ) -> StrResult<Datetime> {
        let time = match (hour, minute, second) {
            (Some(hour), Some(minute), Some(second)) => {
                match time::Time::from_hms(hour.0, minute.0, second.0) {
                    Ok(time) => Some(time),
                    Err(_) => bail!("time is invalid"),
                }
            }
            (None, None, None) => None,
            _ => bail!("time is incomplete"),
        };

        let date = match (year, month, day) {
            (Some(year), Some(month), Some(day)) => {
                match time::Date::from_calendar_date(year.0, month.0, day.0) {
                    Ok(date) => Some(date),
                    Err(_) => bail!("date is invalid"),
                }
            }
            (None, None, None) => None,
            _ => bail!("date is incomplete"),
        };

        Ok(match (date, time) {
            (Some(date), Some(time)) => {
                Datetime::Datetime(PrimitiveDateTime::new(date, time))
            }
            (Some(date), None) => Datetime::Date(date),
            (None, Some(time)) => Datetime::Time(time),
            (None, None) => {
                bail!("at least one of date or time must be fully specified")
            }
        })
    }

    /// Returns the current date.
    ///
    /// ```example
    /// Today's date is
    /// #datetime.today().display().
    /// ```
    #[func]
    pub fn today(
        /// The virtual machine.
        vm: &mut Vm,
        /// An offset to apply to the current UTC date. If set to `{auto}`, the
        /// offset will be the local offset.
        #[named]
        #[default]
        offset: Smart<i64>,
    ) -> StrResult<Datetime> {
        Ok(vm
            .vt
            .world
            .today(offset.as_custom())
            .ok_or("unable to get the current date")?)
    }

    /// Displays the datetime in a specified format.
    ///
    /// Depending on whether you have defined just a date, a time or both, the
    /// default format will be different. If you specified a date, it will be
    /// `[[year]-[month]-[day]]`. If you specified a time, it will be
    /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be
    /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`.
    #[func]
    pub fn display(
        &self,
        /// The format used to display the datetime.
        #[default]
        pattern: Smart<DisplayPattern>,
    ) -> StrResult<EcoString> {
        let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
        let result = match pattern {
            Smart::Auto => match self {
                Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
                Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
                Self::Datetime(datetime) => {
                    datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
                }
            },

            Smart::Custom(DisplayPattern(_, format)) => match self {
                Self::Date(date) => date.format(&format),
                Self::Time(time) => time.format(&format),
                Self::Datetime(datetime) => datetime.format(&format),
            },
        };
        result.map(EcoString::from).map_err(format_time_format_error)
    }

    /// The year if it was specified or `{none}`, otherwise.
    #[func]
    pub fn year(&self) -> Option<i32> {
        match self {
            Self::Date(date) => Some(date.year()),
            Self::Time(_) => None,
            Self::Datetime(datetime) => Some(datetime.year()),
        }
    }

    /// The month if it was specified or `{none}`, otherwise.
    #[func]
    pub fn month(&self) -> Option<u8> {
        match self {
            Self::Date(date) => Some(date.month().into()),
            Self::Time(_) => None,
            Self::Datetime(datetime) => Some(datetime.month().into()),
        }
    }

    /// The weekday if it was specified or `{none}`, otherwise.
    #[func]
    pub fn weekday(&self) -> Option<u8> {
        match self {
            Self::Date(date) => Some(date.weekday().number_from_monday()),
            Self::Time(_) => None,
            Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
        }
    }

    /// The day if it was specified or `{none}`, otherwise.
    #[func]
    pub fn day(&self) -> Option<u8> {
        match self {
            Self::Date(date) => Some(date.day()),
            Self::Time(_) => None,
            Self::Datetime(datetime) => Some(datetime.day()),
        }
    }

    /// The hour if it was specified or `{none}`, otherwise.
    #[func]
    pub fn hour(&self) -> Option<u8> {
        match self {
            Self::Date(_) => None,
            Self::Time(time) => Some(time.hour()),
            Self::Datetime(datetime) => Some(datetime.hour()),
        }
    }

    /// The minute if it was specified or `{none}`, otherwise.
    #[func]
    pub fn minute(&self) -> Option<u8> {
        match self {
            Self::Date(_) => None,
            Self::Time(time) => Some(time.minute()),
            Self::Datetime(datetime) => Some(datetime.minute()),
        }
    }

    /// The second if it was specified or `{none}`, otherwise.
    #[func]
    pub fn second(&self) -> Option<u8> {
        match self {
            Datetime::Date(_) => None,
            Self::Time(time) => Some(time.second()),
            Self::Datetime(datetime) => Some(datetime.second()),
        }
    }

    /// The ordinal (day of the year), if it exists.
    #[func]
    pub fn ordinal(&self) -> Option<u16> {
        match self {
            Self::Datetime(datetime) => Some(datetime.ordinal()),
            Self::Date(date) => Some(date.ordinal()),
            Self::Time(_) => None,
        }
    }
}

impl Repr for Datetime {
    fn repr(&self) -> EcoString {
        let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr()));
        let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr()));
        let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr()));
        let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr()));
        let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr()));
        let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr()));
        let filtered = [year, month, day, hour, minute, second]
            .into_iter()
            .flatten()
            .collect::<EcoVec<_>>();

        eco_format!("datetime{}", &pretty_array_like(&filtered, false))
    }
}

impl PartialOrd for Datetime {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match (self, other) {
            (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
            (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
            (Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
            _ => None,
        }
    }
}

impl Add<Duration> for Datetime {
    type Output = Self;

    fn add(self, rhs: Duration) -> Self::Output {
        let rhs: time::Duration = rhs.into();
        match self {
            Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
            Self::Date(date) => Self::Date(date + rhs),
            Self::Time(time) => Self::Time(time + rhs),
        }
    }
}

impl Sub<Duration> for Datetime {
    type Output = Self;

    fn sub(self, rhs: Duration) -> Self::Output {
        let rhs: time::Duration = rhs.into();
        match self {
            Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
            Self::Date(date) => Self::Date(date - rhs),
            Self::Time(time) => Self::Time(time - rhs),
        }
    }
}

impl Sub for Datetime {
    type Output = StrResult<Duration>;

    fn sub(self, rhs: Self) -> Self::Output {
        match (self, rhs) {
            (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
            (Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
            (Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
            (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
        }
    }
}

pub struct YearComponent(i32);
pub struct MonthComponent(Month);
pub struct DayComponent(u8);
pub struct HourComponent(u8);
pub struct MinuteComponent(u8);
pub struct SecondComponent(u8);

cast! {
    YearComponent,
    v: i32 => Self(v),
}

cast! {
    MonthComponent,
    v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
}

cast! {
    DayComponent,
    v: u8 => Self(v),
}

cast! {
    HourComponent,
    v: u8 => Self(v),
}

cast! {
    MinuteComponent,
    v: u8 => Self(v),
}

cast! {
    SecondComponent,
    v: u8 => Self(v),
}

/// A format in which a datetime can be displayed.
pub struct DisplayPattern(Str, format_description::OwnedFormatItem);

cast! {
    DisplayPattern,
    self => self.0.into_value(),
    v: Str => {
        let item = format_description::parse_owned::<2>(&v)
            .map_err(format_time_invalid_format_description_error)?;
        Self(v, item)
    }
}

/// Format the `Format` error of the time crate in an appropriate way.
fn format_time_format_error(error: Format) -> EcoString {
    match error {
        Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
        Format::InsufficientTypeInformation { .. } => {
            "failed to format datetime (insufficient information)".into()
        }
        err => eco_format!("failed to format datetime in the requested format ({err})"),
    }
}

/// Format the `InvalidFormatDescription` error of the time crate in an
/// appropriate way.
fn format_time_invalid_format_description_error(
    error: InvalidFormatDescription,
) -> EcoString {
    match error {
        InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
            eco_format!("missing closing bracket for bracket at index {}", index)
        }
        InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
            eco_format!("invalid component name '{}' at index {}", name, index)
        }
        InvalidFormatDescription::InvalidModifier { value, index, .. } => {
            eco_format!("invalid modifier '{}' at index {}", value, index)
        }
        InvalidFormatDescription::Expected { what, index, .. } => {
            eco_format!("expected {} at index {}", what, index)
        }
        InvalidFormatDescription::MissingComponentName { index, .. } => {
            eco_format!("expected component name at index {}", index)
        }
        InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
            eco_format!(
                "missing required modifier {} for component at index {}",
                name,
                index
            )
        }
        InvalidFormatDescription::NotSupported { context, what, index, .. } => {
            eco_format!("{} is not supported in {} at index {}", what, context, index)
        }
        err => eco_format!("failed to parse datetime format ({err})"),
    }
}
