///|
/// A geometry table whose rows retain the physical SHP/DBF record alignment.
pub(all) struct Dataset {
  kind : ShapeType
  shapes : Array[Shape]
  table : @dbf.Table
  prj : String?
} derive(Debug)

///|
pub(all) struct DatasetFiles {
  shp : Bytes
  shx : Bytes
  dbf : Bytes
  prj : String?
} derive(Debug)

///|
pub fn Dataset::validate(self : Dataset) -> Unit raise ShapeError {
  if self.shapes.length() != self.table.rows.length() {
    raise InvalidData(0, "geometry and DBF row counts differ")
  }
  for s in self.shapes {
    if s.kind != Null && s.kind != self.kind {
      raise InvalidData(0, "dataset shape type mismatch")
    }
    s.validate()
  }
  ignore(@dbf.validate_table(self.table)) catch {
    _ => raise InvalidData(0, "invalid DBF table")
  }
}

///|
pub fn read_dataset(
  shp : Bytes,
  shx : Bytes,
  dbf : Bytes,
  encoding? : @dbf.Encoding = @dbf.Utf8,
  prj? : String? = None,
) -> Dataset raise ShapeError {
  let sf = read_shp(shp)
  validate_shx(shp, shx)
  let table = @dbf.read(dbf, encoding~) catch {
    _ => raise InvalidData(0, "invalid DBF bytes")
  }
  let d : Dataset = {
    kind: sf.kind,
    shapes: sf.records.map(r => r.shape),
    table,
    prj,
  }
  d.validate()
  d
}

///|
pub fn Dataset::write_dataset(
  self : Dataset,
  encoding? : @dbf.Encoding = @dbf.Utf8,
) -> DatasetFiles raise ShapeError {
  self.validate()
  let files = write_shp(self.kind, self.shapes)
  let dbf = @dbf.write(self.table, encoding~) catch {
    _ => raise InvalidData(0, "DBF table cannot be encoded")
  }
  { shp: files.shp, shx: files.shx, dbf, prj: self.prj }
}

///|
pub fn Dataset::select_indices(
  self : Dataset,
  indices : Array[Int],
) -> Dataset raise ShapeError {
  let shapes : Array[Shape] = []
  let rows : Array[@dbf.Row] = []
  for i in indices {
    if i < 0 || i >= self.shapes.length() {
      raise InvalidData(i, "dataset index outside rows")
    }
    shapes.push(self.shapes[i])
    rows.push(self.table.rows[i])
  }
  {
    kind: self.kind,
    shapes,
    table: { fields: self.table.fields, rows },
    prj: self.prj,
  }
}

///|
pub fn Dataset::filter_bbox(
  self : Dataset,
  bounds : Bounds,
  include_deleted? : Bool = false,
) -> Dataset raise ShapeError {
  bounds.validate()
  let indices : Array[Int] = []
  for i in 0.. Dataset raise ShapeError {
  let index = @dbf.field_index(self.table, field)
  let indices : Array[Int] = []
  match index {
    None => raise InvalidData(0, "unknown DBF field")
    Some(j) =>
      for i in 0.. Dataset raise ShapeError {
  let table = @dbf.project(self.table, names) catch {
    _ => raise InvalidData(0, "unknown projected field")
  }
  { kind: self.kind, shapes: self.shapes, table, prj: self.prj }
}

///|
pub fn Dataset::to_geojson(
  self : Dataset,
  allow_projected? : Bool = false,
  include_deleted? : Bool = false,
) -> Json raise ShapeError {
  self.validate()
  let features : Array[Json] = []
  for i in 0..