///|
pub(all) struct DatasetSummary {
  features : Int32
  null_geometries : Int32
  coordinates : Int32
  envelope : Bounds?
  geometry_types : Array[(String, Int32)]
  columns : Array[ColumnSummary]
} derive(Eq, Debug, ToJson, @json.FromJson)

///|
pub(all) struct ColumnSummary {
  name : String
  type_name : String
  nulls : Int32
  present : Int32
  minimum : String?
  maximum : String?
  total_bytes : Int32
} derive(Eq, Debug, ToJson, @json.FromJson)

///|
fn compare_properties(
  left : Property,
  right : Property,
) -> Int32 raise FgbError {
  match (left, right) {
    (Integer(a), Integer(b)) => a.compare(b)
    (Unsigned(a), Unsigned(b)) => a.compare(b)
    (Real(a), Real(b)) => a.compare(b)
    (Text(a), Text(b)) => a.compare(b)
    (Boolean(a), Boolean(b)) =>
      (if a { 1 } else { 0 }).compare(if b { 1 } else { 0 })
    _ => raise InvalidArgument("incomparable property types")
  }
}

///|
fn property_text(value : Property) -> String? {
  match value {
    Integer(n) => Some(n.to_string())
    Unsigned(n) => Some(n.to_string())
    Real(n) => Some(n.to_string())
    Boolean(n) => Some(n.to_string())
    Text(n) => Some(n)
    Null | Binary(_) => None
  }
}

///|
pub fn Dataset::summarize(
  self : Dataset,
  limits~ : Limits = Limits::default(),
) -> DatasetSummary raise FgbError {
  limits.validate()
  validate_header(self.header, limits~)
  enforce(self.features.length(), limits.max_features, "summary feature count")
  let types : Map[String, Int32] = Map([])
  let mut null_geometries = 0
  let mut coordinates = 0
  let nulls = Array::make(self.header.columns.length(), 0)
  let present = Array::make(self.header.columns.length(), 0)
  let minimum : Array[Property?] = Array::make(
    self.header.columns.length(),
    None,
  )
  let maximum : Array[Property?] = Array::make(
    self.header.columns.length(),
    None,
  )
  let byte_counts = Array::make(self.header.columns.length(), 0)
  for feature in self.features {
    match feature.geometry {
      None => null_geometries += 1
      Some(g) => {
        g.validate(limits~)
        coordinates = checked_add(coordinates, g.coordinate_count())
        let name = g.kind.name()
        types[name] = types.get(name).unwrap_or(0) + 1
      }
    }
    if feature.properties.length() != self.header.columns.length() {
      raise InvalidArgument("summary property count differs from schema")
    }
    for i in 0.. minimum[i] = Some(value)
        Some(old) if compare_properties(value, old) < 0 =>
          minimum[i] = Some(value)
        _ => ()
      }
      match maximum[i] {
        None => maximum[i] = Some(value)
        Some(old) if compare_properties(value, old) > 0 =>
          maximum[i] = Some(value)
        _ => ()
      }
    }
  }
  let columns : Array[ColumnSummary] = []
  for i in 0.. None
        Some(value) => property_text(value)
      },
      maximum: match maximum[i] {
        None => None
        Some(value) => property_text(value)
      },
      total_bytes: byte_counts[i],
    })
  }
  let type_counts = types.to_array()
  type_counts.sort_by((a, b) => a.0.compare(b.0))
  {
    features: self.features.length(),
    null_geometries,
    coordinates,
    envelope: dataset_bounds(self.features),
    geometry_types: type_counts,
    columns,
  }
}

///|
pub(all) struct ArchiveSummary {
  file_bytes : Int32
  header_bytes : Int32
  index_bytes : Int32
  feature_bytes : Int32
  minimum_record_bytes : Int32
  maximum_record_bytes : Int32
  dataset : DatasetSummary
} derive(Eq, Debug, ToJson, @json.FromJson)

///|
pub fn inspect_archive(
  data : Bytes,
  limits~ : Limits = Limits::default(),
) -> ArchiveSummary raise FgbError {
  let reader = Reader::open(data, limits~)
  let dataset = reader.read_all()
  reader.rewind()
  let mut minimum = 0
  let mut maximum = 0
  while reader.next() is Some(record) {
    if minimum == 0 || record.byte_length < minimum {
      minimum = record.byte_length
    }
    if record.byte_length > maximum {
      maximum = record.byte_length
    }
  }
  {
    file_bytes: data.length(),
    header_bytes: reader.index_start - 8,
    index_bytes: reader.index_size,
    feature_bytes: data.length() - reader.features_start,
    minimum_record_bytes: minimum,
    maximum_record_bytes: maximum,
    dataset: dataset.summarize(limits~),
  }
}