// Aggregate archive statistics.
//
// `archive_stats` walks an in-memory archive once and collects
// counts, byte totals, the type histogram, the set of distinct
// target URIs and the earliest/latest WARC-Date. Dates are compared
// by their present fields only, so a coarser date sorts before a
// finer one that begins the same way.

///|
/// Aggregate statistics over an archive.
pub struct ArchiveStats {
  record_count : Int
  total_record_bytes : Int64
  total_block_bytes : Int64
  type_counts : Array[(String, Int)]
  distinct_targets : Array[String]
  earliest : WarcDate?
  latest : WarcDate?
}

///|
/// Compute aggregate statistics over an archive.
pub fn archive_stats(a : WarcArchive) -> ArchiveStats {
  let type_counts : Array[(String, Int)] = []
  let distinct_targets : Array[String] = []
  let mut total_record = 0L
  let mut total_block = 0L
  let mut earliest : WarcDate? = None
  let mut latest : WarcDate? = None
  for i = 0; i < a.record_count(); i = i + 1 {
    let rec = a.record(i).unwrap()
    total_record = total_record + rec.to_bytes().length().to_int64()
    total_block = total_block + rec.block_length().to_int64()
    let t = match rec.record_type() {
      Some(x) => x.type_name()
      None => "unknown"
    }
    bump_count(type_counts, t)
    let target = field_first_of(rec.fields, "WARC-Target-URI")
    match target {
      Some(v) =>
        match parse_uri_ref(v, i.to_int64()) {
          Ok(interior) =>
            if !contains_string(distinct_targets, interior) {
              distinct_targets.push(interior)
            }
          Err(_) => ()
        }
      None => ()
    }
    match rec.warc_date(i.to_int64()) {
      Ok(d) => {
        let key = date_sort_key(d)
        match earliest {
          Some(e) => if key < date_sort_key(e) { earliest = Some(d) }
          None => earliest = Some(d)
        }
        match latest {
          Some(l) => if key > date_sort_key(l) { latest = Some(d) }
          None => latest = Some(d)
        }
      }
      Err(_) => ()
    }
  }
  {
    record_count: a.record_count(),
    total_record_bytes: total_record,
    total_block_bytes: total_block,
    type_counts,
    distinct_targets,
    earliest,
    latest,
  }
}

///|
/// The number of records in the archive.
pub fn ArchiveStats::record_count(self : ArchiveStats) -> Int {
  self.record_count
}

///|
/// The total serialized size of all records in octets.
pub fn ArchiveStats::total_record_bytes(self : ArchiveStats) -> Int64 {
  self.total_record_bytes
}

///|
/// The total size of all content blocks in octets.
pub fn ArchiveStats::total_block_bytes(self : ArchiveStats) -> Int64 {
  self.total_block_bytes
}

///|
/// The number of records of the given type name (lowercase); records
/// of unknown WARC-Type count as `unknown`.
pub fn ArchiveStats::type_count(self : ArchiveStats, name : String) -> Int {
  for i = 0; i < self.type_counts.length(); i = i + 1 {
    let (k, c) = self.type_counts[i]
    if k == name {
      return c
    }
  }
  0
}

///|
/// The number of distinct WARC-Target-URI values.
pub fn ArchiveStats::distinct_target_count(self : ArchiveStats) -> Int {
  self.distinct_targets.length()
}

///|
/// The earliest WARC-Date seen, when any record carried a parseable
/// one.
pub fn ArchiveStats::earliest_date(self : ArchiveStats) -> WarcDate? {
  self.earliest
}

///|
/// The latest WARC-Date seen, when any record carried a parseable
/// one.
pub fn ArchiveStats::latest_date(self : ArchiveStats) -> WarcDate? {
  self.latest
}

///|
/// Increment the count for a key in an ordered (key, count) list.
fn bump_count(counts : Array[(String, Int)], name : String) -> Unit {
  for i = 0; i < counts.length(); i = i + 1 {
    let (k, c) = counts[i]
    if k == name {
      counts[i] = (k, c + 1)
      return
    }
  }
  counts.push((name, 1))
}

///|
/// True when the string list contains the value.
fn contains_string(xs : Array[String], x : String) -> Bool {
  for i = 0; i < xs.length(); i = i + 1 {
    if xs[i] == x {
      return true
    }
  }
  false
}

///|
/// The value of an optional field, or a fallback when absent.
fn opt_or(v : Int?, fallback : Int) -> Int {
  match v {
    Some(x) => x
    None => fallback
  }
}

///|
/// A sort key over the present fields of a date, most significant
/// first. Fields absent at coarser granularities count as zero.
fn date_sort_key(d : WarcDate) -> Int64 {
  d.year.to_int64() * 10000000000L +
  opt_or(d.month, 0).to_int64() * 100000000L +
  opt_or(d.day, 0).to_int64() * 1000000L +
  opt_or(d.hour, 0).to_int64() * 10000L +
  opt_or(d.minute, 0).to_int64() * 100L +
  opt_or(d.second, 0).to_int64()
}