///|
fn conversion_error(message : String) -> FirmwareError {
  FirmwareError::new(ImageInvalidRange, message, SourcePosition::line(0))
}

///|
fn join_record_lines(lines : Array[String]) -> String {
  let mut result = ""
  for index, line in lines {
    if index > 0 {
      result = result + "\n"
    }
    result = result + line
  }
  result
}

///|
fn encode_intel_output_record(
  record_type : Byte,
  address : Int,
  data : Bytes,
) -> String {
  let body = Bytes::makei(data.length() + 4, index => {
    if index == 0 {
      data.length().to_byte()
    } else if index == 1 {
      ((address >> 8) & 0xFF).to_byte()
    } else if index == 2 {
      (address & 0xFF).to_byte()
    } else if index == 3 {
      record_type
    } else {
      data[index - 4]
    }
  })
  let checksum = Bytes::makei(1, _ => twos_complement_checksum(body))
  ":" + encode_hex_bytes(body) + encode_hex_bytes(checksum)
}

///|
/// Deterministically export an image as 32-bit Intel HEX.
pub fn FirmwareImage::to_intel_hex(
  self : FirmwareImage,
  record_bytes? : Int = 16,
) -> Result[String, FirmwareError] {
  if record_bytes <= 0 || record_bytes > 255 {
    return Err(conversion_error("Intel HEX record width must be 1 through 255"))
  }
  match self.highest_address() {
    Some(value) if value > 0xFFFFFFFFUL =>
      return Err(
        conversion_error("Intel HEX output is limited to 32-bit addresses"),
      )
    _ => ()
  }
  match self.entry_point() {
    Some(value) if value > 0xFFFFFFFFUL =>
      return Err(conversion_error("Intel HEX entry point exceeds 32-bit range"))
    _ => ()
  }
  let lines : Array[String] = []
  let mut current_upper = 0UL
  for segment in self.segments() {
    let source = segment.data()
    let mut offset = 0
    while offset < source.length() {
      let absolute = segment.address() + offset.to_uint64()
      let upper = absolute >> 16
      if upper != current_upper {
        let metadata = Bytes::makei(2, index => {
          if index == 0 {
            ((upper >> 8) & 0xFFUL).to_byte()
          } else {
            (upper & 0xFFUL).to_byte()
          }
        })
        lines.push(encode_intel_output_record(4, 0, metadata))
        current_upper = upper
      }
      let low = (absolute & 0xFFFFUL).to_int()
      let boundary = 0x10000 - low
      let remaining = source.length() - offset
      let length = if remaining < record_bytes {
        remaining
      } else {
        record_bytes
      }
      let length = if length < boundary { length } else { boundary }
      let data = Bytes::makei(length, index => source[offset + index])
      lines.push(encode_intel_output_record(0, low, data))
      offset = offset + length
    }
  }
  match self.entry_point() {
    Some(value) => {
      let data = Bytes::makei(4, index => {
        let shift = (3 - index) * 8
        ((value >> shift) & 0xFFUL).to_byte()
      })
      lines.push(encode_intel_output_record(5, 0, data))
    }
    None => ()
  }
  lines.push(encode_intel_output_record(1, 0, b""))
  Ok(join_record_lines(lines))
}

///|
fn srecord_output_type(address_bytes : Int, category : Int) -> Int {
  if category == 0 {
    0
  } else if category == 1 {
    address_bytes - 1
  } else if category == 2 {
    if address_bytes == 2 {
      5
    } else {
      6
    }
  } else {
    11 - address_bytes
  }
}

///|
fn encode_srecord_output(
  type_digit : Int,
  address_bytes : Int,
  address : UInt64,
  data : Bytes,
) -> String {
  let count = address_bytes + data.length() + 1
  let body = Bytes::makei(1 + address_bytes + data.length(), index => {
    if index == 0 {
      count.to_byte()
    } else if index <= address_bytes {
      let shift = (address_bytes - index) * 8
      ((address >> shift) & 0xFFUL).to_byte()
    } else {
      data[index - address_bytes - 1]
    }
  })
  let checksum = Bytes::makei(1, _ => ones_complement_checksum(body))
  "S" +
  type_digit.to_string() +
  encode_hex_bytes(body) +
  encode_hex_bytes(checksum)
}

///|
fn ascii_header(text : String) -> Result[Bytes, FirmwareError] {
  if text.length() > 64 {
    return Err(conversion_error("S-record header is limited to 64 ASCII bytes"))
  }
  for index = 0; index < text.length(); index = index + 1 {
    if text[index].to_int() > 0x7F {
      return Err(conversion_error("S-record header must contain ASCII text"))
    }
  }
  Ok(Bytes::makei(text.length(), index => text[index].to_int().to_byte()))
}

///|
/// Deterministically export an image as Motorola S-record text.
pub fn FirmwareImage::to_srecord(
  self : FirmwareImage,
  record_bytes? : Int = 32,
  header? : String = "",
) -> Result[String, FirmwareError] {
  if record_bytes <= 0 || record_bytes > 250 {
    return Err(conversion_error("S-record data width must be 1 through 250"))
  }
  let highest = match (self.highest_address(), self.entry_point()) {
    (Some(left), Some(right)) => if left > right { left } else { right }
    (Some(value), None) | (None, Some(value)) => value
    _ => 0UL
  }
  if highest > 0xFFFFFFFFUL {
    return Err(
      conversion_error("S-record output is limited to 32-bit addresses"),
    )
  }
  let address_bytes = if highest <= 0xFFFFUL {
    2
  } else if highest <= 0xFFFFFFUL {
    3
  } else {
    4
  }
  let lines : Array[String] = []
  if header.length() > 0 {
    let header_data = match ascii_header(header) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    lines.push(encode_srecord_output(0, 2, 0UL, header_data))
  }
  let mut data_records = 0
  for segment in self.segments() {
    let source = segment.data()
    let mut offset = 0
    while offset < source.length() {
      let remaining = source.length() - offset
      let length = if remaining < record_bytes {
        remaining
      } else {
        record_bytes
      }
      let data = Bytes::makei(length, index => source[offset + index])
      lines.push(
        encode_srecord_output(
          srecord_output_type(address_bytes, 1),
          address_bytes,
          segment.address() + offset.to_uint64(),
          data,
        ),
      )
      data_records = data_records + 1
      offset = offset + length
    }
  }
  if data_records > 0xFFFFFF {
    return Err(conversion_error("S-record data record count exceeds 24 bits"))
  }
  let count_width = if data_records <= 0xFFFF { 2 } else { 3 }
  lines.push(
    encode_srecord_output(
      srecord_output_type(count_width, 2),
      count_width,
      data_records.to_uint64(),
      b"",
    ),
  )
  lines.push(
    encode_srecord_output(
      srecord_output_type(address_bytes, 3),
      address_bytes,
      self.entry_point().unwrap_or(0UL),
      b"",
    ),
  )
  Ok(join_record_lines(lines))
}