///|
pub struct RecordCategoryStatistics {
  name_value : String
  record_count_value : Int
  data_bytes_value : Int
} derive(Eq, Debug)

///|
pub struct DocumentStatistics {
  format_value : FirmwareFormat
  total_records_value : Int
  data_records_value : Int
  metadata_records_value : Int
  data_bytes_value : Int
  largest_data_record_value : Int
  entry_point_value : UInt64?
  has_header_value : Bool
  declared_data_records_value : Int?
  category_values : Array[RecordCategoryStatistics]
} derive(Eq, Debug)

///|
fn build_categories(
  names : Array[String],
  counts : Array[Int],
  bytes : Array[Int],
) -> Array[RecordCategoryStatistics] {
  let result : Array[RecordCategoryStatistics] = []
  for index, name in names {
    if counts[index] > 0 {
      result.push({
        name_value: name,
        record_count_value: counts[index],
        data_bytes_value: bytes[index],
      })
    }
  }
  result
}

///|
pub fn analyze_intel_document(
  document : IntelHexDocument,
) -> DocumentStatistics {
  let names = [
    "data", "end_of_file", "extended_segment_address", "start_segment_address", "extended_linear_address",
    "start_linear_address",
  ]
  let counts = Array::make(6, 0)
  let bytes = Array::make(6, 0)
  let records = document.records()
  let mut data_records = 0
  let mut data_bytes = 0
  let mut largest = 0
  for record in records {
    let index = match record.record_type() {
      IntelData => 0
      IntelEndOfFile => 1
      IntelExtendedSegmentAddress => 2
      IntelStartSegmentAddress => 3
      IntelExtendedLinearAddress => 4
      IntelStartLinearAddress => 5
    }
    counts[index] = counts[index] + 1
    bytes[index] = bytes[index] + record.byte_count()
    if record.record_type() == IntelData {
      data_records = data_records + 1
      data_bytes = data_bytes + record.byte_count()
      if record.byte_count() > largest {
        largest = record.byte_count()
      }
    }
  }
  {
    format_value: IntelHexFormat,
    total_records_value: records.length(),
    data_records_value: data_records,
    metadata_records_value: records.length() - data_records,
    data_bytes_value: data_bytes,
    largest_data_record_value: largest,
    entry_point_value: document.entry_point(),
    has_header_value: false,
    declared_data_records_value: None,
    category_values: build_categories(names, counts, bytes),
  }
}

///|
pub fn analyze_srecord_document(
  document : SRecordDocument,
) -> DocumentStatistics {
  let names = [
    "header", "data_16", "data_24", "data_32", "count_16", "count_24", "terminate_32",
    "terminate_24", "terminate_16",
  ]
  let counts = Array::make(9, 0)
  let bytes = Array::make(9, 0)
  let records = document.records()
  let mut data_records = 0
  let mut data_bytes = 0
  let mut largest = 0
  for record in records {
    let index = match record.record_type() {
      SHeader => 0
      SData16 => 1
      SData24 => 2
      SData32 => 3
      SCount16 => 4
      SCount24 => 5
      STerminate32 => 6
      STerminate24 => 7
      STerminate16 => 8
    }
    counts[index] = counts[index] + 1
    bytes[index] = bytes[index] + record.data().length()
    if record.record_type().is_data() {
      data_records = data_records + 1
      data_bytes = data_bytes + record.data().length()
      if record.data().length() > largest {
        largest = record.data().length()
      }
    }
  }
  {
    format_value: SRecordFormat,
    total_records_value: records.length(),
    data_records_value: data_records,
    metadata_records_value: records.length() - data_records,
    data_bytes_value: data_bytes,
    largest_data_record_value: largest,
    entry_point_value: document.entry_point(),
    has_header_value: document.header() is Some(_),
    declared_data_records_value: document.declared_data_count(),
    category_values: build_categories(names, counts, bytes),
  }
}

///|
pub fn RecordCategoryStatistics::name(
  self : RecordCategoryStatistics,
) -> String {
  self.name_value
}

///|
pub fn RecordCategoryStatistics::record_count(
  self : RecordCategoryStatistics,
) -> Int {
  self.record_count_value
}

///|
pub fn RecordCategoryStatistics::data_bytes(
  self : RecordCategoryStatistics,
) -> Int {
  self.data_bytes_value
}

///|
pub fn DocumentStatistics::format(self : DocumentStatistics) -> FirmwareFormat {
  self.format_value
}

///|
pub fn DocumentStatistics::total_records(self : DocumentStatistics) -> Int {
  self.total_records_value
}

///|
pub fn DocumentStatistics::data_records(self : DocumentStatistics) -> Int {
  self.data_records_value
}

///|
pub fn DocumentStatistics::metadata_records(self : DocumentStatistics) -> Int {
  self.metadata_records_value
}

///|
pub fn DocumentStatistics::data_bytes(self : DocumentStatistics) -> Int {
  self.data_bytes_value
}

///|
pub fn DocumentStatistics::largest_data_record(
  self : DocumentStatistics,
) -> Int {
  self.largest_data_record_value
}

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

///|
pub fn DocumentStatistics::has_header(self : DocumentStatistics) -> Bool {
  self.has_header_value
}

///|
pub fn DocumentStatistics::declared_data_records(
  self : DocumentStatistics,
) -> Int? {
  self.declared_data_records_value
}

///|
pub fn DocumentStatistics::categories(
  self : DocumentStatistics,
) -> Array[RecordCategoryStatistics] {
  self.category_values.copy()
}

///|
fn firmware_format_name(format : FirmwareFormat) -> String {
  match format {
    IntelHexFormat => "intel_hex"
    SRecordFormat => "s_record"
  }
}

///|
pub fn render_document_statistics_text(
  statistics : DocumentStatistics,
) -> String {
  let mut result = "firmware-document format=" +
    firmware_format_name(statistics.format()) +
    " records=" +
    statistics.total_records().to_string() +
    " data-records=" +
    statistics.data_records().to_string() +
    " metadata-records=" +
    statistics.metadata_records().to_string() +
    " data-bytes=" +
    statistics.data_bytes().to_string() +
    " largest-data-record=" +
    statistics.largest_data_record().to_string()
  for category in statistics.categories() {
    result = result +
      "\n" +
      category.name() +
      " records=" +
      category.record_count().to_string() +
      " data-bytes=" +
      category.data_bytes().to_string()
  }
  result
}

///|
pub fn render_document_statistics_json(
  statistics : DocumentStatistics,
) -> String {
  let categories : Array[Json] = []
  for category in statistics.categories() {
    categories.push(
      Json::object({
        "name": Json::string(category.name()),
        "record_count": Json::number(category.record_count().to_double()),
        "data_bytes": Json::number(category.data_bytes().to_double()),
      }),
    )
  }
  Json::object({
    "type": Json::string("firmware_document_statistics"),
    "format": Json::string(firmware_format_name(statistics.format())),
    "total_records": Json::number(statistics.total_records().to_double()),
    "data_records": Json::number(statistics.data_records().to_double()),
    "metadata_records": Json::number(statistics.metadata_records().to_double()),
    "data_bytes": Json::number(statistics.data_bytes().to_double()),
    "largest_data_record": Json::number(
      statistics.largest_data_record().to_double(),
    ),
    "entry_point": optional_address_json(statistics.entry_point()),
    "has_header": Json::boolean(statistics.has_header()),
    "declared_data_records": match statistics.declared_data_records() {
      Some(value) => Json::number(value.to_double())
      None => Json::null()
    },
    "categories": Json::array(categories),
  }).stringify()
}