///|
/// A simple DateTime type for cron calculations.
/// The five-argument constructor defaults seconds to zero.
pub struct DateTime {
  year : Int
  month : Int // 1..12
  day : Int // 1..31
  hour : Int // 0..23
  minute : Int // 0..59
  second : Int // 0..59
}

///|
/// Create a new DateTime value.
pub fn DateTime::new(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
) -> DateTime {
  { year, month, day, hour, minute, second: 0 }
}

///|
/// Create a DateTime value with explicit second precision.
pub fn DateTime::new_with_second(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
) -> DateTime {
  { year, month, day, hour, minute, second }
}

///|
/// Compare two DateTime values for equality.
pub fn DateTime::eq(self : DateTime, other : DateTime) -> Bool {
  self.year == other.year &&
  self.month == other.month &&
  self.day == other.day &&
  self.hour == other.hour &&
  self.minute == other.minute &&
  self.second == other.second
}

///|
/// Check if self is before other.
pub fn DateTime::lt(self : DateTime, other : DateTime) -> Bool {
  if self.year != other.year {
    return self.year < other.year
  }
  if self.month != other.month {
    return self.month < other.month
  }
  if self.day != other.day {
    return self.day < other.day
  }
  if self.hour != other.hour {
    return self.hour < other.hour
  }
  if self.minute != other.minute {
    return self.minute < other.minute
  }
  self.second < other.second
}

///|
/// Check if self is after other.
pub fn DateTime::gt(self : DateTime, other : DateTime) -> Bool {
  other.lt(self)
}

///|
/// Check if self is before or equal to other.
pub fn DateTime::le(self : DateTime, other : DateTime) -> Bool {
  self.eq(other) || self.lt(other)
}

///|
/// Check if self is after or equal to other.
pub fn DateTime::ge(self : DateTime, other : DateTime) -> Bool {
  self.eq(other) || self.gt(other)
}

///|
/// Format DateTime as "YYYY-MM-DD HH:MM", adding ":SS" when seconds are non-zero.
pub fn DateTime::to_string(self : DateTime) -> String {
  let year_str = self.year.to_string()
  let month_str = pad2(self.month)
  let day_str = pad2(self.day)
  let hour_str = pad2(self.hour)
  let minute_str = pad2(self.minute)
  let base = year_str +
    "-" +
    month_str +
    "-" +
    day_str +
    " " +
    hour_str +
    ":" +
    minute_str
  if self.second == 0 {
    base
  } else {
    base + ":" + pad2(self.second)
  }
}

///|
/// Format DateTime with an explicit seconds component.
pub fn DateTime::to_string_with_seconds(self : DateTime) -> String {
  if self.second == 0 {
    self.to_string() + ":00"
  } else {
    self.to_string()
  }
}

///|
/// Pad integer to 2-digit string.
fn pad2(n : Int) -> String {
  if n < 10 {
    "0" + n.to_string()
  } else {
    n.to_string()
  }
}

///|
/// Parse a DateTime from "YYYY-MM-DD HH:MM" or "YYYY-MM-DD HH:MM:SS".
pub fn parse_datetime(s : String) -> DateTime? {
  if s.length() != 16 && s.length() != 19 {
    return None
  }
  if s[4].to_int() != 45 ||
    s[7].to_int() != 45 ||
    s[10].to_int() != 32 ||
    s[13].to_int() != 58 {
    return None
  }
  if s.length() == 19 && s[16].to_int() != 58 {
    return None
  }
  let year_str = s[0:4].to_owned()
  let month_str = s[5:7].to_owned()
  let day_str = s[8:10].to_owned()
  let hour_str = s[11:13].to_owned()
  let minute_str = s[14:16].to_owned()

  match parse_int(year_str) {
    Some(year) =>
      match parse_int(month_str) {
        Some(month) =>
          match parse_int(day_str) {
            Some(day) =>
              match parse_int(hour_str) {
                Some(hour) =>
                  match parse_int(minute_str) {
                    Some(minute) => {
                      let second = if s.length() == 19 {
                        match parse_int(s[17:19].to_owned()) {
                          Some(value) => value
                          None => return None
                        }
                      } else {
                        0
                      }
                      let dt = { year, month, day, hour, minute, second }
                      if is_valid_datetime(dt) {
                        Some(dt)
                      } else {
                        None
                      }
                    }
                    None => None
                  }
                None => None
              }
            None => None
          }
        None => None
      }
    None => None
  }
}

///|
/// Parse a positive integer from a string, returning None on failure.
fn parse_int(s : String) -> Int? {
  if s == "" {
    return None
  }
  let mut value = 0
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if code < 48 || code > 57 {
      return None
    }
    value = value * 10 + (code - 48)
  }
  Some(value)
}