///|
/// Automatic width considers both the highest payload byte and the entry point.
pub(all) enum AddressWidth {
  Auto
  Bits16
  Bits24
  Bits32
} derive(Eq, Debug)

///|
/// Configurable canonical single-block output. CS:IP flattening is opt-in.
pub(all) struct WriterOptions {
  data_length : Int
  address_width : AddressWidth
  uppercase : Bool
  line_ending : @codec.LineEnding
  emit_count : Bool
  flatten_segment_entry : Bool
  max_output_length : Int
} derive(Eq, Debug)

///|
/// Sixteen data bytes, the smallest safe width, S5/S6 count and termination.
pub fn WriterOptions::default() -> WriterOptions {
  {
    data_length: 16,
    address_width: Auto,
    uppercase: true,
    line_ending: LF,
    emit_count: true,
    flatten_segment_entry: false,
    max_output_length: 256 * 1024 * 1024,
  }
}

///|
fn select_kind(
  image : @model.FirmwareImage,
  options : WriterOptions,
) -> (Int, Int64) raise @model.FirmwareError {
  let entry = match image.entry {
    Some(Segment(_, _)) if !options.flatten_segment_entry =>
      raise @model.FirmwareError(
        @model.diagnostic(
          InvalidOption,
          "SREC cannot retain CS:IP; explicitly enable segment entry flattening",
        ),
      )
    Some(value) => value.address()
    None => 0L
  }
  let highest = image.memory.highest_address().unwrap_or(0L).max(entry)
  let kind = match options.address_width {
    Auto =>
      if highest <= 65535L {
        1
      } else if highest <= 0xFFFFFFL {
        2
      } else {
        3
      }
    Bits16 => 1
    Bits24 => 2
    Bits32 => 3
  }
  ignore(@codec.address_bytes(highest, kind + 1))
  if options.data_length < 1 || options.data_length > 253 - kind {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "data length exceeds S\{kind} count capacity",
      ),
    )
  }
  (kind, entry)
}

///|
/// Stream canonical records in address order without building text or record arrays.
pub fn for_each_record(
  image : @model.FirmwareImage,
  visit : (Record) -> Unit raise @model.FirmwareError,
  options? : WriterOptions = WriterOptions::default(),
) -> Unit raise @model.FirmwareError {
  let (kind, entry) = select_kind(image, options)
  if image.metadata.header is Some(header) {
    visit(Record::new(0, 0L, header))
  }
  let mut count = 0
  for segment in image.memory.segments() {
    let mut offset = 0
    while offset < segment.data.length() {
      let length = options.data_length.min(segment.data.length() - offset)
      visit(
        Record::new(
          kind,
          segment.start + offset.to_int64(),
          segment.data[offset:offset + length].to_owned(),
        ),
      )
      offset += length
      count += 1
    }
  }
  if options.emit_count {
    if count > 0xFFFFFF {
      raise @model.FirmwareError(
        @model.diagnostic(ResourceLimit, "data record count cannot fit S6"),
      )
    }
    visit(
      Record::new(if count <= 65535 { 5 } else { 6 }, count.to_int64(), b""),
    )
  }
  visit(Record::new(10 - kind, entry, b""))
}

///|
/// Serialize deterministic Motorola S-Record text. Missing entry becomes zero,
/// because every standard termination record contains an address field.
pub fn encode(
  image : @model.FirmwareImage,
  options? : WriterOptions = WriterOptions::default(),
) -> String raise @model.FirmwareError {
  let output = @codec.TextOutput::new(
    options.line_ending,
    options.max_output_length,
  )
  for_each_record(
    image,
    record => output.write_line(record.encode(uppercase=options.uppercase)),
    options~,
  )
  output.finish()
}