///|
pub(all) struct UtcTime {
  hour_value : Int
  minute_value : Int
  second_value : Int
  nanosecond_value : Int
  second_wire_value : ExactDecimal
} derive(Eq, Debug)

///|
pub(all) struct UtcDate {
  year_value : Int
  month_value : Int
  day_value : Int
} derive(Eq, Debug)

///|
pub(all) struct UtcDateTime {
  date_value : UtcDate
  time_value : UtcTime
} derive(Eq, Debug)

///|
fn time_error(source : SourceRef, message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(TimeInvalid, Error, message, source),
  )
}

///|
fn date_error(source : SourceRef, message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(DateInvalid, Error, message, source),
  )
}

///|
fn decimal_pair(
  text : String,
  start : Int,
  source : SourceRef,
) -> Result[Int, NmeaError] {
  if start < 0 || start + 1 >= text.length() {
    return Err(time_error(source, "time or date pair is missing"))
  }
  let first = text[start].to_int()
  let second = text[start + 1].to_int()
  if first < 48 || first > 57 || second < 48 || second > 57 {
    return Err(time_error(source, "time or date contains a non-digit"))
  }
  Ok((first - 48) * 10 + second - 48)
}

///|
fn power10(count : Int) -> Int {
  let mut value = 1
  let mut index = 0
  while index < count {
    value = value * 10
    index = index + 1
  }
  value
}

///|
pub fn UtcTime::parse(
  text : String,
  source : SourceRef,
) -> Result[UtcTime, NmeaError] {
  if text.length() < 6 {
    return Err(time_error(source, "UTC time requires hhmmss"))
  }
  if text.length() > 6 && (text.length() == 7 || text[6].to_int() != 46) {
    return Err(
      time_error(
        source, "UTC fractional seconds require a decimal point and digits",
      ),
    )
  }
  let hour = match decimal_pair(text, 0, source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let minute = match decimal_pair(text, 2, source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let second = match decimal_pair(text, 4, source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if hour > 23 || minute > 59 || second > 60 {
    return Err(time_error(source, "UTC time component is outside its range"))
  }
  let second_text = text[4:].to_owned()
  let second_wire = match ExactDecimal::parse(second_text, max_scale=9) {
    Ok(value) => value
    Err(_) =>
      return Err(time_error(source, "UTC seconds are not a valid decimal"))
  }
  let scale = second_wire.scale()
  let whole_coefficient = second.to_int64() * power10(scale).to_int64()
  let fractional = second_wire.coefficient() - whole_coefficient
  let nanosecond = (fractional * power10(9 - scale).to_int64()).to_int()
  Ok({
    hour_value: hour,
    minute_value: minute,
    second_value: second,
    nanosecond_value: nanosecond,
    second_wire_value: second_wire,
  })
}

///|
pub fn UtcTime::hour(self : UtcTime) -> Int {
  self.hour_value
}

///|
pub fn UtcTime::minute(self : UtcTime) -> Int {
  self.minute_value
}

///|
pub fn UtcTime::second(self : UtcTime) -> Int {
  self.second_value
}

///|
pub fn UtcTime::nanosecond(self : UtcTime) -> Int {
  self.nanosecond_value
}

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

///|
pub fn UtcTime::to_string(self : UtcTime) -> String {
  pad2(self.hour_value) +
  ":" +
  pad2(self.minute_value) +
  ":" +
  self.second_wire_value.to_string()
}

///|
/// Renders the compact hhmmss decimal form used by NMEA fields.
pub fn utc_time_wire(value : UtcTime) -> String {
  pad2(value.hour_value) +
  pad2(value.minute_value) +
  value.second_wire_value.to_string()
}

///|
fn is_leap_year(year : Int) -> Bool {
  year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}

///|
fn days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
pub fn UtcDate::new(
  year : Int,
  month : Int,
  day : Int,
  source : SourceRef,
) -> Result[UtcDate, NmeaError] {
  if year < 1900 || year > 9999 || month < 1 || month > 12 {
    return Err(
      date_error(source, "UTC date year or month is outside its range"),
    )
  }
  let maximum = days_in_month(year, month)
  if day < 1 || day > maximum {
    return Err(date_error(source, "UTC date day is outside its month"))
  }
  Ok({ year_value: year, month_value: month, day_value: day })
}

///|
pub fn UtcDate::parse_rmc(
  text : String,
  source : SourceRef,
) -> Result[UtcDate, NmeaError] {
  if text.length() != 6 {
    return Err(date_error(source, "RMC date requires ddmmyy"))
  }
  let day = match decimal_pair(text, 0, source) {
    Ok(value) => value
    Err(_) => return Err(date_error(source, "RMC day is invalid"))
  }
  let month = match decimal_pair(text, 2, source) {
    Ok(value) => value
    Err(_) => return Err(date_error(source, "RMC month is invalid"))
  }
  let short_year = match decimal_pair(text, 4, source) {
    Ok(value) => value
    Err(_) => return Err(date_error(source, "RMC year is invalid"))
  }
  let year = if short_year <= 79 {
    2000 + short_year
  } else {
    1900 + short_year
  }
  UtcDate::new(year, month, day, source)
}

///|
pub fn UtcDate::year(self : UtcDate) -> Int {
  self.year_value
}

///|
pub fn UtcDate::month(self : UtcDate) -> Int {
  self.month_value
}

///|
pub fn UtcDate::day(self : UtcDate) -> Int {
  self.day_value
}

///|
/// Renders the compact ddmmyy form used by RMC.
pub fn utc_date_rmc_wire(value : UtcDate) -> String {
  pad2(value.day_value) + pad2(value.month_value) + pad2(value.year_value % 100)
}

///|
fn UtcDate::days_since_epoch(self : UtcDate) -> Int64 {
  let mut days : Int64 = 0L
  if self.year_value >= 1970 {
    let mut year = 1970
    while year < self.year_value {
      days = days + (if is_leap_year(year) { 366L } else { 365L })
      year = year + 1
    }
  } else {
    let mut year = self.year_value
    while year < 1970 {
      days = days - (if is_leap_year(year) { 366L } else { 365L })
      year = year + 1
    }
  }
  let mut month = 1
  while month < self.month_value {
    days = days + days_in_month(self.year_value, month).to_int64()
    month = month + 1
  }
  days + (self.day_value - 1).to_int64()
}

///|
pub fn UtcDateTime::new(date : UtcDate, time : UtcTime) -> UtcDateTime {
  { date_value: date, time_value: time }
}

///|
pub fn UtcDateTime::date(self : UtcDateTime) -> UtcDate {
  self.date_value
}

///|
pub fn UtcDateTime::time(self : UtcDateTime) -> UtcTime {
  self.time_value
}

///|
fn UtcDateTime::epoch_nanoseconds(self : UtcDateTime) -> Int64 {
  let day_seconds = self.date_value.days_since_epoch() * 86400L
  let time_seconds = (self.time_value.hour_value * 3600 +
  self.time_value.minute_value * 60 +
  self.time_value.second_value).to_int64()
  (day_seconds + time_seconds) * 1000000000L +
  self.time_value.nanosecond_value.to_int64()
}

///|
pub fn UtcDateTime::millis_since(
  self : UtcDateTime,
  earlier : UtcDateTime,
) -> Int64 {
  (self.epoch_nanoseconds() - earlier.epoch_nanoseconds()) / 1000000L
}

///|
pub fn UtcDateTime::compare(self : UtcDateTime, other : UtcDateTime) -> Int {
  let left = self.epoch_nanoseconds()
  let right = other.epoch_nanoseconds()
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}