///|
pub struct SRecordDocument {
  record_values : Array[SRecord]
  header_value : Bytes?
  chunk_values : Array[FirmwareChunk]
  declared_data_count_value : Int?
  entry_point_value : UInt64?
} derive(Eq, Debug)

///|
fn srecord_document_error(
  code : FirmwareErrorCode,
  message : String,
  line_index : Int,
) -> FirmwareError {
  FirmwareError::new(code, message, SourcePosition::line(line_index))
}

///|
/// Parse a complete S-record document and validate its bookkeeping records.
pub fn parse_srecord_document(
  text : String,
) -> Result[SRecordDocument, FirmwareError] {
  let lines = split_firmware_lines(text)
  let records : Array[SRecord] = []
  let chunks : Array[FirmwareChunk] = []
  let mut header : Bytes? = None
  let mut declared_count : Int? = None
  let mut entry_point : UInt64? = None
  let mut data_count = 0
  let mut widest_data_address = 0
  let mut terminated = false
  for line_index, line in lines {
    if terminated {
      return Err(
        srecord_document_error(
          DocumentRecordAfterTerminator,
          "S-record appears after the termination record",
          line_index,
        ),
      )
    }
    let record = match parse_srecord(line, line_index~) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    records.push(record)
    match record.record_type() {
      SHeader => {
        if header is Some(_) || data_count > 0 {
          return Err(
            srecord_document_error(
              DocumentDuplicateMetadata,
              "S-record header must appear once before data records",
              line_index,
            ),
          )
        }
        header = Some(record.data())
      }
      SData16 | SData24 | SData32 => {
        data_count = data_count + 1
        let width = record.record_type().address_bytes()
        if width > widest_data_address {
          widest_data_address = width
        }
        chunks.push(
          FirmwareChunk::new(record.address(), record.data(), line_index),
        )
      }
      SCount16 | SCount24 => {
        if declared_count is Some(_) {
          return Err(
            srecord_document_error(
              DocumentDuplicateMetadata,
              "S-record data count appears more than once",
              line_index,
            ),
          )
        }
        declared_count = Some(record.address().to_int())
      }
      STerminate16 | STerminate24 | STerminate32 => {
        let expected = match widest_data_address {
          2 => STerminate16
          3 => STerminate24
          4 => STerminate32
          _ => record.record_type()
        }
        if record.record_type() != expected {
          return Err(
            srecord_document_error(
              IntegrityViolation,
              "S-record termination width does not match data address width",
              line_index,
            ),
          )
        }
        if record.address() != 0UL {
          entry_point = Some(record.address())
        } else {
          entry_point = Some(0UL)
        }
        terminated = true
      }
    }
  }
  if !terminated {
    return Err(
      srecord_document_error(
        DocumentMissingTerminator,
        "S-record document is missing its termination record",
        lines.length(),
      ),
    )
  }
  match declared_count {
    Some(expected) if expected != data_count =>
      return Err(
        srecord_document_error(
          IntegrityViolation,
          "S-record declared data count does not match data records",
          0,
        ),
      )
    _ => ()
  }
  Ok({
    record_values: records,
    header_value: header,
    chunk_values: chunks,
    declared_data_count_value: declared_count,
    entry_point_value: entry_point,
  })
}

///|
pub fn SRecordDocument::records(self : SRecordDocument) -> Array[SRecord] {
  self.record_values.copy()
}

///|
pub fn SRecordDocument::header(self : SRecordDocument) -> Bytes? {
  match self.header_value {
    Some(data) => Some(Bytes::makei(data.length(), index => data[index]))
    None => None
  }
}

///|
pub fn SRecordDocument::data_chunks(
  self : SRecordDocument,
) -> Array[FirmwareChunk] {
  self.chunk_values.copy()
}

///|
pub fn SRecordDocument::declared_data_count(self : SRecordDocument) -> Int? {
  self.declared_data_count_value
}

///|
pub fn SRecordDocument::entry_point(self : SRecordDocument) -> UInt64? {
  self.entry_point_value
}