///|
pub struct FrameBatchItem {
  index_value : Int
  frame_value : CanFrame
  decoded_value : DecodedFrame?
  error_value : DbcError?
} derive(Eq, Debug)

///|
pub struct MessageFrameCount {
  message_id_value : Int
  message_name_value : String
  count_value : Int
} derive(Eq, Debug)

///|
pub struct FrameBatchResult {
  items_value : Array[FrameBatchItem]
  accepted_value : Int
  rejected_value : Int
  total_data_bytes_value : Int
  message_counts_value : Array[MessageFrameCount]
} derive(Eq, Debug)

///|
pub fn FrameBatchItem::index(self : FrameBatchItem) -> Int {
  self.index_value
}

///|
pub fn FrameBatchItem::frame(self : FrameBatchItem) -> CanFrame {
  self.frame_value
}

///|
pub fn FrameBatchItem::accepted(self : FrameBatchItem) -> Bool {
  self.decoded_value is Some(_)
}

///|
pub fn FrameBatchItem::decoded(self : FrameBatchItem) -> DecodedFrame? {
  self.decoded_value
}

///|
pub fn FrameBatchItem::error(self : FrameBatchItem) -> DbcError? {
  self.error_value
}

///|
pub fn MessageFrameCount::message_id(self : MessageFrameCount) -> Int {
  self.message_id_value
}

///|
pub fn MessageFrameCount::message_name(self : MessageFrameCount) -> String {
  self.message_name_value
}

///|
pub fn MessageFrameCount::count(self : MessageFrameCount) -> Int {
  self.count_value
}

///|
pub fn FrameBatchResult::items(
  self : FrameBatchResult,
) -> Array[FrameBatchItem] {
  self.items_value.copy()
}

///|
pub fn FrameBatchResult::accepted(self : FrameBatchResult) -> Int {
  self.accepted_value
}

///|
pub fn FrameBatchResult::rejected(self : FrameBatchResult) -> Int {
  self.rejected_value
}

///|
pub fn FrameBatchResult::total_data_bytes(self : FrameBatchResult) -> Int {
  self.total_data_bytes_value
}

///|
pub fn FrameBatchResult::acceptance_ratio(self : FrameBatchResult) -> Double {
  let total = self.accepted_value + self.rejected_value
  if total == 0 {
    1.0
  } else {
    self.accepted_value.to_double() / total.to_double()
  }
}

///|
pub fn FrameBatchResult::message_counts(
  self : FrameBatchResult,
) -> Array[MessageFrameCount] {
  self.message_counts_value.copy()
}

///|
pub fn FrameBatchResult::find_message_count(
  self : FrameBatchResult,
  message_id : Int,
) -> MessageFrameCount? {
  for count in self.message_counts_value {
    if count.message_id() == message_id {
      return Some(count)
    }
  }
  None
}

///|
pub fn FrameBatchResult::find_item(
  self : FrameBatchResult,
  index : Int,
) -> FrameBatchItem? {
  for item in self.items_value {
    if item.index() == index {
      return Some(item)
    }
  }
  None
}

///|
fn increment_message_count(
  counts : Array[MessageFrameCount],
  message : DbcMessage,
) -> Unit {
  for index, count in counts {
    if count.message_id() == message.id() {
      counts[index] = {
        message_id_value: count.message_id(),
        message_name_value: count.message_name(),
        count_value: count.count() + 1,
      }
      return
    }
  }
  counts.push({
    message_id_value: message.id(),
    message_name_value: message.name(),
    count_value: 1,
  })
}

///|
pub fn decode_frame_batch(
  database : DbcDatabase,
  frames : Array[CanFrame],
  max_frames? : Int = 100000,
  max_total_data_bytes? : Int = 16 * 1024 * 1024,
) -> Result[FrameBatchResult, DbcError] {
  if max_frames < 0 || max_total_data_bytes < 0 {
    return Err(
      DbcError::new(
        InputLimitExceeded,
        "frame batch limits cannot be negative",
        1,
        1,
      ),
    )
  }
  if frames.length() > max_frames {
    return Err(
      DbcError::new(
        InputLimitExceeded,
        "frame batch count exceeds configured limit",
        1,
        1,
      ),
    )
  }
  let mut total_bytes = 0
  for frame in frames {
    total_bytes = total_bytes + frame.data().length()
    if total_bytes > max_total_data_bytes {
      return Err(
        DbcError::new(
          InputLimitExceeded,
          "frame batch bytes exceed configured limit",
          1,
          1,
        ),
      )
    }
  }
  let items : Array[FrameBatchItem] = []
  let counts : Array[MessageFrameCount] = []
  let mut accepted = 0
  let mut rejected = 0
  for index, frame in frames {
    match decode_frame(database, frame) {
      Ok(decoded) => {
        accepted = accepted + 1
        increment_message_count(counts, decoded.message())
        items.push({
          index_value: index,
          frame_value: frame,
          decoded_value: Some(decoded),
          error_value: None,
        })
      }
      Err(error) => {
        rejected = rejected + 1
        items.push({
          index_value: index,
          frame_value: frame,
          decoded_value: None,
          error_value: Some(error),
        })
      }
    }
  }
  Ok({
    items_value: items,
    accepted_value: accepted,
    rejected_value: rejected,
    total_data_bytes_value: total_bytes,
    message_counts_value: counts,
  })
}

///|
pub fn render_frame_batch_text(result : FrameBatchResult) -> String {
  let mut output = "Frame batch: accepted=\{result.accepted()}, rejected=\{result.rejected()}, bytes=\{result.total_data_bytes()}\n"
  for count in result.message_counts() {
    output = output +
      "message \{count.message_id()} \{count.message_name()}: \{count.count()} frame(s)\n"
  }
  for item in result.items() {
    if !item.accepted() {
      match item.error() {
        Some(error) =>
          output = output +
            "rejected index=\{item.index()} id=\{item.frame().id()}: \{error.code()}\n"
        None => ()
      }
    }
  }
  output
}

///|
pub fn render_frame_batch_json(result : FrameBatchResult) -> String {
  let mut output = "{\"accepted\":\{result.accepted()},\"rejected\":\{result.rejected()},\"total_data_bytes\":\{result.total_data_bytes()},\"message_counts\":["
  for index, count in result.message_counts() {
    if index > 0 {
      output = output + ","
    }
    output = output +
      "{\"message_id\":\{count.message_id()},\"message_name\":" +
      json_string(count.message_name()) +
      ",\"count\":\{count.count()}}"
  }
  output = output + "],\"rejections\":["
  let mut rejection_index = 0
  for item in result.items() {
    match item.error() {
      Some(error) => {
        if rejection_index > 0 {
          output = output + ","
        }
        output = output +
          "{\"index\":\{item.index()},\"message_id\":\{item.frame().id()},\"code\":" +
          json_string(error.code()) +
          ",\"message\":" +
          json_string(error.message()) +
          "}"
        rejection_index = rejection_index + 1
      }
      None => ()
    }
  }
  output + "]}"
}