///|
pub(all) struct DateTime {
  year : Int
  month : Int
  day : Int
  hour : Int
  minute : Int
  second : Int
} derive(Debug, ToJson)

///|
fn month_days(year : Int, month : Int) -> Int {
  if month == 2 {
    if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
      29
    } else {
      28
    }
  } else if month == 4 || month == 6 || month == 9 || month == 11 {
    30
  } else {
    31
  }
}

///|
fn DateTime::validate(self : DateTime) -> Unit raise EdfError {
  if self.year < 1985 ||
    self.year > 2084 ||
    self.month < 1 ||
    self.month > 12 ||
    self.day < 1 ||
    self.day > month_days(self.year, self.month) ||
    self.hour < 0 ||
    self.hour > 23 ||
    self.minute < 0 ||
    self.minute > 59 ||
    self.second < 0 ||
    self.second > 59 {
    raise Invalid("date/time outside supported Gregorian 1985..2084 range")
  }
}

///|
fn two(n : Int) -> String {
  if n < 10 {
    "0" + n.to_string()
  } else {
    n.to_string()
  }
}

///|
pub fn DateTime::date_field(self : DateTime) -> String raise EdfError {
  self.validate()
  "\{two(self.day)}.\{two(self.month)}.\{two(self.year%100)}"
}

///|
pub fn DateTime::time_field(self : DateTime) -> String raise EdfError {
  self.validate()
  "\{two(self.hour)}.\{two(self.minute)}.\{two(self.second)}"
}

///|
fn DateTime::long_date(self : DateTime) -> String {
  let months = [
    "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV",
    "DEC",
  ]
  "\{two(self.day)}-\{months[self.month-1]}-\{self.year}"
}

///|
pub fn parse_datetime(date : String, time : String) -> DateTime raise EdfError {
  if date.length() != 8 ||
    time.length() != 8 ||
    date[2] != '.' ||
    date[5] != '.' ||
    time[2] != '.' ||
    time[5] != '.' {
    raise Invalid("date/time must be dd.mm.yy and hh.mm.ss")
  }
  let yy = integer(date[6:8].to_owned())
  let dt : DateTime = {
    year: if yy >= 85 {
      1900 + yy
    } else {
      2000 + yy
    },
    month: integer(date[3:5].to_owned()),
    day: integer(date[0:2].to_owned()),
    hour: integer(time[0:2].to_owned()),
    minute: integer(time[3:5].to_owned()),
    second: integer(time[6:8].to_owned()),
  }
  dt.validate()
  dt
}

///|
pub fn DateTime::advance(
  self : DateTime,
  seconds : Int,
) -> DateTime raise EdfError {
  self.validate()
  if seconds < 0 {
    raise Invalid("calendar advance must be nonnegative")
  }
  let total = self.hour.to_int64() * 3600L +
    self.minute.to_int64() * 60L +
    self.second.to_int64() +
    seconds.to_int64()
  let mut days = (total / 86400L).to_int()
  let rest = (total % 86400L).to_int()
  let mut y = self.year
  let mut m = self.month
  let mut d = self.day
  while days > 0 {
    let left = month_days(y, m) - d
    if days <= left {
      d += days
      days = 0
    } else {
      days -= left + 1
      d = 1
      m += 1
      if m == 13 {
        m = 1
        y += 1
      }
      if y > 2084 {
        raise Limit("cropped date exceeds 2084")
      }
    }
  }
  {
    year: y,
    month: m,
    day: d,
    hour: rest / 3600,
    minute: rest % 3600 / 60,
    second: rest % 60,
  }
}