// Validation for publicly constructible TZif data values.

///|
fn invalid_data(message : String) -> @types.TzifResult[Unit] {
  Err(InvalidTransitionTable(message))
}

///|
fn validate_abbreviation(
  abbr : String,
  label : String,
) -> @types.TzifResult[Unit] {
  for index in 0.. '~'.to_int() {
      return invalid_data("\{label} must contain printable ASCII only")
    }
  }
  Ok(())
}

///|
fn validate_posix_abbreviation(
  abbr : String,
  label : String,
) -> @types.TzifResult[Unit] {
  if abbr.length() < 3 || abbr.length() > 255 {
    return invalid_data("\{label} length must be in 3..255 bytes")
  }
  for index in 0..= 'a'.to_int() && code <= 'z'.to_int()) ||
      (code >= 'A'.to_int() && code <= 'Z'.to_int())
    let is_digit = code >= '0'.to_int() && code <= '9'.to_int()
    if !is_letter && !is_digit && code != '+'.to_int() && code != '-'.to_int() {
      return invalid_data(
        "\{label} must contain only ASCII alphanumeric, '+' or '-'",
      )
    }
  }
  Ok(())
}

///|
fn validate_posix_date_rule(
  date_rule : @types.PosixDateRule,
) -> @types.TzifResult[Unit] {
  match date_rule {
    JulianNoLeap(day) =>
      if day < 1 || day > 365 {
        invalid_data("POSIX Julian date rule must be in 1..365")
      } else {
        Ok(())
      }
    DayOfYear(day) =>
      if day < 0 || day > 365 {
        invalid_data("POSIX day-of-year rule must be in 0..365")
      } else {
        Ok(())
      }
    MonthWeekDay(month, week, weekday) =>
      if month < 1 ||
        month > 12 ||
        week < 1 ||
        week > 5 ||
        weekday < 0 ||
        weekday > 6 {
        invalid_data("POSIX M rule contains an out-of-range calendar field")
      } else {
        Ok(())
      }
  }
}

///|
fn validate_posix_transition_rule(
  rule : @types.PosixTransitionRule,
  _version : Byte,
) -> @types.TzifResult[Unit] {
  match validate_posix_date_rule(rule.date_rule) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let maximum = 167 * 3600 + 59 * 60 + 59
  let minimum = -maximum
  if rule.time_of_day < minimum || rule.time_of_day > maximum {
    return invalid_data(
      "POSIX transition clock is outside this TZif version range",
    )
  }
  Ok(())
}

///|
fn validate_posix_rule(
  rule : @types.PosixTzRule,
  version : Byte,
) -> @types.TzifResult[Unit] {
  match
    validate_posix_abbreviation(rule.std_abbr, "POSIX standard abbreviation") {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  if rule.std_offset < -89_999 || rule.std_offset > 89_999 {
    return invalid_data("POSIX standard offset is outside the supported range")
  }
  match rule.dst_abbr {
    None =>
      if rule.dst_offset is Some(_) ||
        rule.start_rule is Some(_) ||
        rule.end_rule is Some(_) {
        return invalid_data("POSIX non-DST rule must not contain DST fields")
      }
    Some(dst_abbr) => {
      match
        validate_posix_abbreviation(dst_abbr, "POSIX daylight abbreviation") {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      let dst_offset = match rule.dst_offset {
        None =>
          return invalid_data("POSIX DST abbreviation requires a DST offset")
        Some(value) => value
      }
      if dst_offset < -89_999 || dst_offset > 89_999 {
        return invalid_data(
          "POSIX daylight offset is outside the supported range",
        )
      }
      let start_rule = match rule.start_rule {
        None =>
          return invalid_data("POSIX DST abbreviation requires a start rule")
        Some(value) => value
      }
      let end_rule = match rule.end_rule {
        None =>
          return invalid_data("POSIX DST abbreviation requires an end rule")
        Some(value) => value
      }
      match validate_posix_transition_rule(start_rule, version) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match validate_posix_transition_rule(end_rule, version) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
  }
  Ok(())
}

///|
fn leap_posix_boundary(occurrence : Int64, previous_correction : Int) -> Int64? {
  let correction = previous_correction.to_int64()
  if correction < 0L && occurrence > 9_223_372_036_854_775_807L + correction {
    return None
  }
  let boundary = occurrence - correction
  if boundary < 0L {
    None
  } else {
    Some(boundary)
  }
}

///|
fn is_utc_month_boundary(timestamp : Int64) -> Bool {
  if timestamp % 86_400L != 0L {
    return false
  }
  let days = timestamp / 86_400L
  let shifted = days + 719_468L
  let era = shifted / 146_097L
  let day_of_era = shifted - era * 146_097L
  let year_of_era = (
      day_of_era -
      day_of_era / 1_460L +
      day_of_era / 36_524L -
      day_of_era / 146_096L
    ) /
    365L
  let day_of_year = day_of_era -
    (365L * year_of_era + year_of_era / 4L - year_of_era / 100L)
  let month_prime = (5L * day_of_year + 2L) / 153L
  let day = day_of_year - (153L * month_prime + 2L) / 5L + 1L
  day == 1L
}

///|
fn validate_leap_seconds(
  leap_seconds : Array[@types.LeapSecond],
) -> @types.TzifResult[Unit] {
  let mut previous_occurrence = 0L
  let mut previous_correction = 0
  for index in 0.. 0 &&
      (
        leap.transition_time <= previous_occurrence ||
        leap.transition_time - previous_occurrence < 2_419_199L
      ) {
      return invalid_data(
        "adjacent leap-second occurrences must be at least 2419199 seconds apart",
      )
    }
    match leap_posix_boundary(leap.transition_time, previous_correction) {
      Some(boundary) =>
        if !is_utc_month_boundary(boundary) {
          return invalid_data(
            "leap-second occurrence must be at the end of a UTC month",
          )
        }
      None =>
        return invalid_data(
          "leap-second occurrence cannot be represented as a POSIX UTC boundary",
        )
    }
    if index == 0 && leap.correction != 1 && leap.correction != -1 {
      return invalid_data("first leap-second correction must be either 1 or -1")
    }
    if index > 0 {
      let correction_delta = leap.correction.to_int64() -
        previous_correction.to_int64()
      if correction_delta != 1L && correction_delta != -1L {
        return invalid_data(
          "adjacent leap-second corrections must differ by exactly 1",
        )
      }
    }
    previous_occurrence = leap.transition_time
    previous_correction = leap.correction
  }
  Ok(())
}

///|
fn validate_indicators(tz : @types.TzifData) -> @types.TzifResult[Unit] {
  match tz.standard_indicators {
    None => ()
    Some(values) =>
      if values.length() != tz.time_types.length() {
        return invalid_data(
          "ttisstd indicator count must equal time type count",
        )
      }
  }
  match tz.utc_indicators {
    None => ()
    Some(values) =>
      if values.length() != tz.time_types.length() {
        return invalid_data("ttisut indicator count must equal time type count")
      }
  }
  match tz.utc_indicators {
    None => ()
    Some(utc_values) =>
      for index in 0..
              if !standard_values[index] {
                return invalid_data(
                  "ttisut=1 requires the corresponding ttisstd indicator to be 1",
                )
              }
            None =>
              return invalid_data(
                "ttisut=1 requires an explicit corresponding ttisstd=1 indicator",
              )
          }
        }
      }
  }
  Ok(())
}

///|
fn validate_footer_consistency(tz : @types.TzifData) -> @types.TzifResult[Unit] {
  if tz.transitions.length() == 0 || tz.posix_rule is None {
    return Ok(())
  }
  let last_transition = tz.transitions[tz.transitions.length() - 1]
  let explicit_type = tz.time_types[last_transition.type_index]
  let footer_type = match
    @converter.active_offset(tz, last_transition.utc_time) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if footer_type.utoff != explicit_type.utoff ||
    footer_type.is_dst != explicit_type.is_dst ||
    footer_type.abbr != explicit_type.abbr {
    return invalid_data(
      "POSIX footer is inconsistent with the final explicit transition type",
    )
  }
  Ok(())
}

///|
/// Validate a user-constructed `TzifData` before caching, serializing, or
/// passing it across a trust boundary.
///
/// `load` already applies equivalent structural checks to file input.  This
/// function exists because every field of `TzifData` is intentionally public
/// for inspection and fixture construction, so callers can also create an
/// invalid in-memory value without parsing bytes first.
pub fn validate(tz : @types.TzifData) -> @types.TzifResult[Unit] {
  if tz.version != b'2' && tz.version != b'3' {
    return Err(UnsupportedVersion(tz.version))
  }
  if tz.time_types.length() == 0 {
    return invalid_data("TZif must contain at least one local time type")
  }
  if tz.transitions.length() > 1_000_000 {
    return Err(
      ResourceLimitExceeded(
        "TZif transition count exceeds the default limit of 1000000",
      ),
    )
  }
  if tz.time_types.length() > 256 {
    return Err(
      ResourceLimitExceeded(
        "TZif time type count exceeds the default limit of 256",
      ),
    )
  }
  if tz.leap_seconds.length() > 10_000 {
    return Err(
      ResourceLimitExceeded(
        "TZif leap-second count exceeds the default limit of 10000",
      ),
    )
  }
  let mut designation_bytes = 0
  for index in 0.. 1_048_576 - time_type.abbr.length() - 1 {
      return Err(
        ResourceLimitExceeded(
          "TZif designation data exceeds the default limit of 1048576 bytes",
        ),
      )
    }
    designation_bytes = designation_bytes + time_type.abbr.length() + 1
    match validate_abbreviation(time_type.abbr, "time type abbreviation") {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  let mut previous_transition = 0L
  for index in 0.. 0 && transition.utc_time <= previous_transition {
      return invalid_data("transition times must be strictly increasing")
    }
    if transition.type_index < 0 ||
      transition.type_index >= tz.time_types.length() {
      return invalid_data(
        "transition type index \{transition.type_index} is out of range",
      )
    }
    previous_transition = transition.utc_time
  }
  match validate_leap_seconds(tz.leap_seconds) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  match validate_indicators(tz) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  match tz.posix_rule {
    None => ()
    Some(rule) =>
      match validate_posix_rule(rule, tz.version) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
  }
  validate_footer_consistency(tz)
}