// Calendar dates used by Beancount directives.  No timezone is involved.

///|
pub(all) struct Date {
  year : Int32
  month : Int32
  day : Int32
}

///|
pub fn Date::is_leap(year : Int32) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
pub fn Date::days_in_month(year : Int32, month : Int32) -> Int32 {
  if month == 2 {
    if Date::is_leap(year) {
      29
    } else {
      28
    }
  } else if month == 4 || month == 6 || month == 9 || month == 11 {
    30
  } else {
    31
  }
}

///|
pub fn Date::new(year : Int32, month : Int32, day : Int32) -> Date? {
  if year < 1 || year > 9999 {
    return None
  }
  if month < 1 || month > 12 {
    return None
  }
  if day < 1 || day > Date::days_in_month(year, month) {
    return None
  }
  Some({ year, month, day, })
}

///|
fn parse_fixed_digits(text : String, start : Int32, count : Int32) -> Int32? {
  let mut value = 0
  let mut i = 0
  while i < count {
    let c = match text.get_char(start + i) {
      Some(c) => c
      None => return None
    }
    if !c.is_ascii_digit() {
      return None
    }
    value = value * 10 + (c.to_int() - '0'.to_int())
    i = i + 1
  }
  Some(value)
}

///|
pub fn Date::parse(text : String) -> Date? {
  let value = text
  if value.length() != 10 ||
    value.get_char(4) != Some('-') ||
    value.get_char(7) != Some('-') {
    return None
  }
  let year = match parse_fixed_digits(value, 0, 4) {
    Some(v) => v
    None => return None
  }
  let month = match parse_fixed_digits(value, 5, 2) {
    Some(v) => v
    None => return None
  }
  let day = match parse_fixed_digits(value, 8, 2) {
    Some(v) => v
    None => return None
  }
  Date::new(year, month, day)
}

///|
fn pad2(value : Int32) -> String {
  if value < 10 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
pub fn Date::to_string(self : Date) -> String {
  let y = if self.year < 1000 {
    "0".repeat(4 - self.year.to_string().length()) + self.year.to_string()
  } else {
    self.year.to_string()
  }
  y + "-" + pad2(self.month) + "-" + pad2(self.day)
}

///|
pub fn Date::compare(a : Date, b : Date) -> Int32 {
  if a.year != b.year {
    a.year.compare(b.year)
  } else if a.month != b.month {
    a.month.compare(b.month)
  } else {
    a.day.compare(b.day)
  }
}

///|
pub fn Date::equal(a : Date, b : Date) -> Bool {
  a.compare(b) == 0
}

///|
pub fn Date::less_equal(a : Date, b : Date) -> Bool {
  a.compare(b) <= 0
}

///|
pub fn Date::less_than(a : Date, b : Date) -> Bool {
  a.compare(b) < 0
}

///|
pub fn Date::day_of_year(self : Date) -> Int32 {
  let mut total = self.day
  let mut month = 1
  while month < self.month {
    total = total + Date::days_in_month(self.year, month)
    month = month + 1
  }
  total
}