///|
pub struct BatchRecord {
  index : Int
  message : GS1Message?
  error : String?
} derive(Debug, Eq)

///|
pub struct BatchReport {
  total : Int
  succeeded : Int
  failed : Int
  records : Array[BatchRecord]
} derive(Debug, Eq)

///|
pub fn parse_batch(input : String) -> BatchReport {
  let records : Array[BatchRecord] = []
  let mut start = 0
  let mut index = 0
  let len = input.length()
  for pos = 0; pos < len; pos = pos + 1 {
    if input.get_char(pos) == Some('\n') {
      let line = input.unsafe_substring(start~, end=pos).trim().to_owned()
      if !line.is_empty() {
        records.push(parse_batch_record(index, line))
        index = index + 1
      }
      start = pos + 1
    }
  }
  if start < len {
    let line = input.unsafe_substring(start~, end=len).trim().to_owned()
    if !line.is_empty() {
      records.push(parse_batch_record(index, line))
      index = index + 1
    }
  }
  let succeeded = records.fold(init=0, fn(acc, record) {
    if record.message is Some(_) {
      acc + 1
    } else {
      acc
    }
  })
  {
    total: records.length(),
    succeeded,
    failed: records.length() - succeeded,
    records,
  }
}

///|
fn parse_batch_record(index : Int, line : String) -> BatchRecord {
  try {
    let message = parse_gs1(line)
    let report = message.validate()
    if report.ok {
      { index, message: Some(message), error: None }
    } else {
      { index, message: None, error: Some(report.summary()) }
    }
  } catch {
    error => { index, message: None, error: Some(error_string(error)) }
  }
}

///|
pub fn BatchReport::successes(self : BatchReport) -> Array[GS1Message] {
  let out : Array[GS1Message] = []
  for record in self.records {
    match record.message {
      Some(message) => out.push(message)
      None => ()
    }
  }
  out
}

///|
pub fn BatchReport::errors(self : BatchReport) -> Array[String] {
  let out : Array[String] = []
  for record in self.records {
    match record.error {
      Some(error) => out.push(error)
      None => ()
    }
  }
  out
}

///|
fn error_string(error : ParseError) -> String {
  match error {
    ParseError::UnknownAI(code) => "unknown AI \{code}"
    ParseError::Truncated(code, expected, actual) =>
      "truncated AI \{code}: expected \{expected}, got \{actual}"
    ParseError::InvalidSyntax(message) => message
  }
}