///|
/// Validated wire record. Address is the 16-bit offset, not an absolute address.
pub struct Record {
  kind : Int
  address : Int
  data : Bytes
} derive(Eq, Debug)

///|
/// Validate a record's structural fields, independent of document state.
pub fn Record::new(
  kind : Int,
  address : Int,
  data : Bytes,
) -> Record raise @model.FirmwareError {
  let location : @codec.Location = {
    format: IntelHex,
    line: 0,
    column_offset: 0,
    record_type: Some(kind),
  }
  check_fields(kind, address, data.length(), location)
  { kind, address, data, }
}

///|
fn check_fields(
  kind : Int,
  address : Int,
  length : Int,
  location : @codec.Location,
) -> Unit raise @model.FirmwareError {
  if kind < 0 || kind > 5 {
    raise location.error(
      UnsupportedRecord,
      8,
      "unsupported Intel HEX record type \{kind}",
    )
  }
  if address < 0 || address > 65535 {
    raise location.error(AddressOverflow, 4, "record offset must fit 16 bits")
  }
  if length > 255 {
    raise location.error(InvalidLength, 2, "record data exceeds 255 bytes")
  }
  let expected = match kind {
    0 => length
    1 => 0
    2 | 4 => 2
    _ => 4
  }
  if length != expected {
    raise location.error(
      InvalidLength,
      2,
      "record type \{kind} requires \{expected} data bytes, found \{length}",
    )
  }
  if kind != 0 && address != 0 {
    raise location.error(
      InvalidRecord,
      4,
      "control record address must be zero",
    )
  }
}

///|
/// Parse one unpadded record, always enforcing checksum and structural lengths.
pub fn parse_line(
  text : String,
  line? : Int = 1,
) -> Record raise @model.FirmwareError {
  parse_at(text, {
    format: IntelHex,
    line,
    column_offset: 0,
    record_type: None,
  })
}

///|
fn parse_at(
  text : String,
  initial : @codec.Location,
) -> Record raise @model.FirmwareError {
  if text.length() > 521 {
    raise initial.error(
      ResourceLimit,
      1,
      "Intel HEX record exceeds 521 characters",
    )
  }
  if !text.has_prefix(":") {
    raise initial.error(
      InvalidRecord,
      1,
      "expected ':' at start of Intel HEX record",
    )
  }
  let bytes = @codec.decode_hex(text, 1, initial)
  if bytes.length() < 5 {
    raise initial.error(InvalidLength, 2, "truncated Intel HEX record")
  }
  let count = bytes[0].to_int()
  let kind = bytes[3].to_int()
  let location = { ..initial, record_type: Some(kind), }
  if bytes.length() != count + 5 {
    raise location.error(
      InvalidLength,
      2,
      "expected \{count} data bytes but found \{bytes.length() - 5}",
    )
  }
  if !verify_checksum(bytes) {
    raise location.error(
      ChecksumMismatch,
      text.length() - 1,
      "checksum mismatch",
    )
  }
  let address = bytes[1].to_int() * 256 + bytes[2].to_int()
  check_fields(kind, address, count, location)
  { kind, address, data: bytes[4:4 + count].to_owned(), }
}