///|
pub struct MxRecordData {
  preference_value : Int
  exchange_value : DomainName
} derive(Eq, Debug)

///|
pub struct SrvRecordData {
  priority_value : Int
  weight_value : Int
  port_value : Int
  target_value : DomainName
} derive(Eq, Debug)

///|
pub struct CaaRecordData {
  flags_value : Int
  tag_value : String
  value_value : String
} derive(Eq, Debug)

///|
pub struct SoaRecordData {
  primary_name_value : DomainName
  responsible_name_value : DomainName
  serial_value : UInt64
  refresh_value : Ttl
  retry_value : Ttl
  expire_value : Ttl
  minimum_value : Ttl
} derive(Eq, Debug)

///|
pub struct GenericRecordData {
  record_type_value : String
  field_values : Array[String]
} derive(Eq, Debug)

///|
pub(all) enum RecordData {
  AData(Ipv4Address)
  AaaaData(Ipv6Address)
  NsData(DomainName)
  CnameData(DomainName)
  PtrData(DomainName)
  MxData(MxRecordData)
  TxtData(Array[String])
  SrvData(SrvRecordData)
  CaaData(CaaRecordData)
  SoaData(SoaRecordData)
  GenericData(GenericRecordData)
} derive(Eq, Debug)

///|
fn record_data_error(
  code : ZoneErrorCode,
  message : String,
  token : ZoneToken,
) -> ZoneError {
  ZoneError::new(code, message, token.span())
}

///|
fn require_record_fields(
  fields : Array[ZoneToken],
  count : Int,
  record_type : ZoneToken,
) -> Result[Unit, ZoneError] {
  if fields.length() == count {
    Ok(())
  } else {
    Err(
      record_data_error(
        MissingRecordField,
        "record data has an invalid number of fields",
        record_type,
      ),
    )
  }
}

///|
fn parse_bounded_decimal(
  token : ZoneToken,
  maximum : UInt64,
) -> Result[UInt64, ZoneError] {
  let text = token.text()
  if text.length() == 0 {
    return Err(
      record_data_error(InvalidRecordData, "numeric field is empty", token),
    )
  }
  let mut value = 0UL
  for index = 0; index < text.length(); index = index + 1 {
    let code = text[index].to_int()
    if code < 48 || code > 57 {
      return Err(
        record_data_error(
          InvalidRecordData,
          "numeric record field must contain decimal digits",
          token,
        ),
      )
    }
    let digit = (code - 48).to_uint64()
    if value > maximum / 10UL || value * 10UL > maximum - digit {
      return Err(
        record_data_error(
          NumericFieldOutOfRange,
          "numeric record field exceeds its allowed range",
          token,
        ),
      )
    }
    value = value * 10UL + digit
  }
  Ok(value)
}

///|
fn parse_target_name(
  token : ZoneToken,
  origin : DomainName,
) -> Result[DomainName, ZoneError] {
  match resolve_zone_name(token.text(), origin) {
    Ok(value) => Ok(value)
    Err(error) =>
      Err(ZoneError::new(InvalidRecordData, error.message(), token.span()))
  }
}

///|
fn parse_single_name_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
  origin : DomainName,
) -> Result[DomainName, ZoneError] {
  match require_record_fields(fields, 1, record_type) {
    Ok(_) => parse_target_name(fields[0], origin)
    Err(error) => Err(error)
  }
}

///|
fn parse_mx_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
  origin : DomainName,
) -> Result[RecordData, ZoneError] {
  match require_record_fields(fields, 2, record_type) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let preference = match parse_bounded_decimal(fields[0], 65535UL) {
    Ok(value) => value.to_int()
    Err(error) => return Err(error)
  }
  let exchange = match parse_target_name(fields[1], origin) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(MxData({ preference_value: preference, exchange_value: exchange }))
}

///|
fn parse_txt_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
) -> Result[RecordData, ZoneError] {
  if fields.length() == 0 {
    return Err(
      record_data_error(
        MissingRecordField,
        "TXT record requires at least one character string",
        record_type,
      ),
    )
  }
  let values : Array[String] = []
  for field in fields {
    if field.text().length() > 255 {
      return Err(
        record_data_error(
          InvalidRecordData,
          "TXT character string exceeds 255 octets",
          field,
        ),
      )
    }
    values.push(field.text())
  }
  Ok(TxtData(values))
}

///|
fn parse_srv_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
  origin : DomainName,
) -> Result[RecordData, ZoneError] {
  match require_record_fields(fields, 4, record_type) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let values : Array[Int] = []
  for index = 0; index < 3; index = index + 1 {
    match parse_bounded_decimal(fields[index], 65535UL) {
      Ok(value) => values.push(value.to_int())
      Err(error) => return Err(error)
    }
  }
  let target = match parse_target_name(fields[3], origin) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(
    SrvData({
      priority_value: values[0],
      weight_value: values[1],
      port_value: values[2],
      target_value: target,
    }),
  )
}

///|
fn valid_caa_tag(text : String) -> Bool {
  if text.length() == 0 || text.length() > 15 {
    return false
  }
  for index = 0; index < text.length(); index = index + 1 {
    let code = text[index].to_int()
    if !((code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      (code >= 48 && code <= 57) ||
      code == 45) {
      return false
    }
  }
  true
}

///|
fn parse_caa_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
) -> Result[RecordData, ZoneError] {
  match require_record_fields(fields, 3, record_type) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let flags = match parse_bounded_decimal(fields[0], 255UL) {
    Ok(value) => value.to_int()
    Err(error) => return Err(error)
  }
  if !valid_caa_tag(fields[1].text()) {
    return Err(
      record_data_error(InvalidRecordData, "CAA tag is invalid", fields[1]),
    )
  }
  Ok(
    CaaData({
      flags_value: flags,
      tag_value: ascii_lower(fields[1].text()),
      value_value: fields[2].text(),
    }),
  )
}

///|
fn parse_soa_ttl(token : ZoneToken) -> Result[Ttl, ZoneError] {
  match parse_ttl(token.text()) {
    Ok(value) => Ok(value)
    Err(error) =>
      Err(ZoneError::new(InvalidRecordData, error.message(), token.span()))
  }
}

///|
fn parse_soa_data(
  fields : Array[ZoneToken],
  record_type : ZoneToken,
  origin : DomainName,
) -> Result[RecordData, ZoneError] {
  match require_record_fields(fields, 7, record_type) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let primary = match parse_target_name(fields[0], origin) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let responsible = match parse_target_name(fields[1], origin) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let serial = match parse_bounded_decimal(fields[2], 0xFFFFFFFFUL) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let timers : Array[Ttl] = []
  for index = 3; index < 7; index = index + 1 {
    match parse_soa_ttl(fields[index]) {
      Ok(value) => timers.push(value)
      Err(error) => return Err(error)
    }
  }
  Ok(
    SoaData({
      primary_name_value: primary,
      responsible_name_value: responsible,
      serial_value: serial,
      refresh_value: timers[0],
      retry_value: timers[1],
      expire_value: timers[2],
      minimum_value: timers[3],
    }),
  )
}

///|
fn valid_generic_type(text : String) -> Bool {
  if text.length() == 0 {
    return false
  }
  for index = 0; index < text.length(); index = index + 1 {
    let code = text[index].to_int()
    if !((code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      (code >= 48 && code <= 57) ||
      code == 45) {
      return false
    }
  }
  true
}

///|
fn uppercase_ascii(text : String) -> String {
  let mut result = ""
  for index = 0; index < text.length(); index = index + 1 {
    let code = text[index].to_int()
    let value = if code >= 97 && code <= 122 { code - 32 } else { code }
    result = result + ascii_character(value)
  }
  result
}

///|
fn generic_record_data(
  record_type : ZoneToken,
  fields : Array[ZoneToken],
) -> Result[RecordData, ZoneError] {
  if !valid_generic_type(record_type.text()) {
    return Err(
      record_data_error(
        InvalidRecordType,
        "record type contains invalid characters",
        record_type,
      ),
    )
  }
  if fields.length() == 0 {
    return Err(
      record_data_error(
        MissingRecordField,
        "generic record data is empty",
        record_type,
      ),
    )
  }
  let values : Array[String] = []
  for field in fields {
    values.push(field.text())
  }
  Ok(
    GenericData({
      record_type_value: uppercase_ascii(record_type.text()),
      field_values: values,
    }),
  )
}

///|
pub fn parse_record_data(
  record_type : ZoneToken,
  fields : Array[ZoneToken],
  origin : DomainName,
) -> Result[RecordData, ZoneError] {
  match ascii_lower(record_type.text()) {
    "a" => {
      match require_record_fields(fields, 1, record_type) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match parse_ipv4(fields[0].text()) {
        Ok(value) => Ok(AData(value))
        Err(error) =>
          Err(
            ZoneError::new(InvalidRecordData, error.message(), fields[0].span()),
          )
      }
    }
    "aaaa" => {
      match require_record_fields(fields, 1, record_type) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match parse_ipv6(fields[0].text()) {
        Ok(value) => Ok(AaaaData(value))
        Err(error) =>
          Err(
            ZoneError::new(InvalidRecordData, error.message(), fields[0].span()),
          )
      }
    }
    "ns" =>
      match parse_single_name_data(fields, record_type, origin) {
        Ok(value) => Ok(NsData(value))
        Err(error) => Err(error)
      }
    "cname" =>
      match parse_single_name_data(fields, record_type, origin) {
        Ok(value) => Ok(CnameData(value))
        Err(error) => Err(error)
      }
    "ptr" =>
      match parse_single_name_data(fields, record_type, origin) {
        Ok(value) => Ok(PtrData(value))
        Err(error) => Err(error)
      }
    "mx" => parse_mx_data(fields, record_type, origin)
    "txt" => parse_txt_data(fields, record_type)
    "srv" => parse_srv_data(fields, record_type, origin)
    "caa" => parse_caa_data(fields, record_type)
    "soa" => parse_soa_data(fields, record_type, origin)
    _ => generic_record_data(record_type, fields)
  }
}

///|
pub fn parse_record_data_statement(
  statement : ZoneStatement,
  origin : DomainName,
) -> Result[RecordData, ZoneError] {
  let tokens = statement.tokens()
  if tokens.length() == 0 {
    return Err(
      ZoneError::new(
        MissingRecordField,
        "record data statement is empty",
        SourceSpan::point(0, 0),
      ),
    )
  }
  let fields : Array[ZoneToken] = []
  for index = 1; index < tokens.length(); index = index + 1 {
    fields.push(tokens[index])
  }
  parse_record_data(tokens[0], fields, origin)
}

///|
pub fn MxRecordData::preference(self : MxRecordData) -> Int {
  self.preference_value
}

///|
pub fn MxRecordData::exchange(self : MxRecordData) -> DomainName {
  self.exchange_value
}

///|
pub fn SrvRecordData::priority(self : SrvRecordData) -> Int {
  self.priority_value
}

///|
pub fn SrvRecordData::weight(self : SrvRecordData) -> Int {
  self.weight_value
}

///|
pub fn SrvRecordData::port(self : SrvRecordData) -> Int {
  self.port_value
}

///|
pub fn SrvRecordData::target(self : SrvRecordData) -> DomainName {
  self.target_value
}

///|
pub fn CaaRecordData::flags(self : CaaRecordData) -> Int {
  self.flags_value
}

///|
pub fn CaaRecordData::tag(self : CaaRecordData) -> String {
  self.tag_value
}

///|
pub fn CaaRecordData::value(self : CaaRecordData) -> String {
  self.value_value
}

///|
pub fn SoaRecordData::primary_name(self : SoaRecordData) -> DomainName {
  self.primary_name_value
}

///|
pub fn SoaRecordData::responsible_name(self : SoaRecordData) -> DomainName {
  self.responsible_name_value
}

///|
pub fn SoaRecordData::serial(self : SoaRecordData) -> UInt64 {
  self.serial_value
}

///|
pub fn SoaRecordData::refresh(self : SoaRecordData) -> Ttl {
  self.refresh_value
}

///|
pub fn SoaRecordData::retry(self : SoaRecordData) -> Ttl {
  self.retry_value
}

///|
pub fn SoaRecordData::expire(self : SoaRecordData) -> Ttl {
  self.expire_value
}

///|
pub fn SoaRecordData::minimum(self : SoaRecordData) -> Ttl {
  self.minimum_value
}

///|
pub fn GenericRecordData::record_type(self : GenericRecordData) -> String {
  self.record_type_value
}

///|
pub fn GenericRecordData::fields(self : GenericRecordData) -> Array[String] {
  self.field_values.copy()
}