///|
pub(all) struct FrameLimits {
  max_sentence_bytes : Int
  max_fields : Int
  max_field_bytes : Int
} derive(Eq, Debug)

///|
pub fn FrameLimits::standard() -> FrameLimits {
  { max_sentence_bytes: 1024, max_fields: 64, max_field_bytes: 256 }
}

///|
pub(all) struct NmeaFrame {
  prefix_value : UInt16
  talker_value : String
  formatter_value : String
  fields_value : Array[String]
  checksum_status_value : ChecksumStatus
  checksum_value : Int?
} derive(Eq, Debug)

///|
pub fn NmeaFrame::new(
  prefix : UInt16,
  talker : String,
  formatter : String,
  fields : Array[String],
) -> NmeaFrame {
  {
    prefix_value: prefix,
    talker_value: talker,
    formatter_value: formatter,
    fields_value: fields.copy(),
    checksum_status_value: NotChecked,
    checksum_value: None,
  }
}

///|
pub fn NmeaFrame::prefix(self : NmeaFrame) -> UInt16 {
  self.prefix_value
}

///|
pub fn NmeaFrame::talker(self : NmeaFrame) -> String {
  self.talker_value
}

///|
pub fn NmeaFrame::formatter(self : NmeaFrame) -> String {
  self.formatter_value
}

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

///|
pub fn NmeaFrame::checksum_status(self : NmeaFrame) -> ChecksumStatus {
  self.checksum_status_value
}

///|
pub fn NmeaFrame::encode(self : NmeaFrame) -> String {
  let mut payload = self.talker_value + self.formatter_value
  for field in self.fields_value {
    payload = payload + "," + field
  }
  (if self.prefix_value == 36 { "$" } else { "!" }) +
  payload +
  "*" +
  NmeaChecksum::hex(NmeaChecksum::calculate(payload)) +
  "\r\n"
}

///|
fn frame_error(code : NmeaErrorCode, message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(code, Error, message, SourceRef::sentence(0)),
  )
}

///|
fn trim_line_end(text : String) -> String {
  let mut end = text.length()
  while end > 0 &&
        (text[end - 1].to_int() == 10 || text[end - 1].to_int() == 13) {
    end = end - 1
  }
  text[:end].to_owned()
}

///|
/// Parses one complete caller-provided NMEA frame.
pub fn parse_frame(
  input : String,
  policy? : ParsePolicy = Strict,
  limits? : FrameLimits = FrameLimits::standard(),
) -> Result[NmeaFrame, NmeaError] {
  let text = trim_line_end(input)
  if text.length() < 2 {
    return Err(frame_error(FramingMissingStart, "sentence is too short"))
  }
  if text.length() > limits.max_sentence_bytes {
    return Err(
      frame_error(FramingTooLong, "sentence exceeds configured length"),
    )
  }
  let prefix = text[0]
  if prefix != 36 && prefix != 33 {
    return Err(
      frame_error(FramingInvalidPrefix, "sentence must start with $ or !"),
    )
  }
  let body = text[1:]
  let mut star = -1
  for index = 0; index < body.length(); index = index + 1 {
    if body[index].to_int() == 42 {
      star = index
      break
    }
  }
  let (payload, checksum_status, checksum) = if star < 0 {
    if policy is Strict {
      return Err(
        frame_error(ChecksumMissing, "strict policy requires a checksum"),
      )
    }
    (body.to_owned(), MissingChecksum, None)
  } else {
    let payload = body[:star].to_owned()
    let expected_text = body[star + 1:].to_owned()
    let expected = match checksum_value(expected_text) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let actual = NmeaChecksum::calculate(payload)
    if actual != expected {
      return Err(
        frame_error(ChecksumMismatch, "checksum does not match payload"),
      )
    }
    (payload, PresentValid, Some(expected))
  }
  let parts : Array[String] = []
  for part in payload.split(",") {
    parts.push(part.to_owned())
  }
  if parts.length() == 0 || parts[0].length() < 5 {
    return Err(
      frame_error(
        SentenceInvalid,
        "sentence formatter must contain talker and formatter",
      ),
    )
  }
  let identifier = parts[0]
  let talker = identifier[:2].to_owned()
  let formatter = identifier[2:].to_owned()
  if formatter.length() == 0 {
    return Err(frame_error(SentenceInvalid, "sentence formatter is empty"))
  }
  let fields : Array[String] = []
  for index = 1; index < parts.length(); index = index + 1 {
    let field = parts[index]
    if field.length() > limits.max_field_bytes {
      return Err(frame_error(FramingTooLong, "field exceeds configured length"))
    }
    fields.push(field)
  }
  if fields.length() > limits.max_fields {
    return Err(frame_error(FramingTooLong, "sentence contains too many fields"))
  }
  Ok({
    prefix_value: prefix,
    talker_value: talker,
    formatter_value: formatter,
    fields_value: fields,
    checksum_status_value: checksum_status,
    checksum_value: checksum,
  })
}