///|
/// Parsed file header; ranges remain available for inspection.
pub(all) struct Header {
  file_length : Int
  kind : ShapeType
  bounds : Bounds
  zmin : Double
  zmax : Double
  mmin : Double
  mmax : Double
} derive(Debug)

///|
/// Resource limits apply before allocating point or record arrays.
pub(all) struct ReadLimits {
  max_records : Int
  max_points_per_record : Int
  max_parts_per_record : Int
  max_file_bytes : Int
} derive(Debug)

///|
pub fn ReadLimits::default() -> ReadLimits {
  {
    max_records: 1000000,
    max_points_per_record: 1000000,
    max_parts_per_record: 100000,
    max_file_bytes: 536870912,
  }
}

///|
fn ReadLimits::validate(self : ReadLimits) -> Unit raise ShapeError {
  if self.max_records < 0 ||
    self.max_points_per_record < 0 ||
    self.max_parts_per_record < 0 ||
    self.max_file_bytes < 100 {
    raise InvalidData(0, "invalid resource limits")
  }
}

///|
/// Read and verify the 100-byte header. Full file bytes are required.
pub fn read_header(data : Bytes) -> Header raise ShapeError {
  let r = ByteReader::new(data)
  r.require(100)
  if r.be32() != 9994 {
    raise InvalidData(0, "invalid Shapefile magic")
  }
  for _ in 0..<5 {
    if r.be32() != 0 {
      raise InvalidData(r.pos - 4, "reserved header word must be zero")
    }
  }
  let file_length = words_to_bytes(r.be32(), 24)
  if file_length != data.length() {
    raise InvalidData(24, "declared file length differs from actual bytes")
  }
  if r.le32() != 1000 {
    raise InvalidData(28, "unsupported Shapefile version")
  }
  let kind = shape_type(r.le32())
  let xmin = r.f64()
  let ymin = r.f64()
  let xmax = r.f64()
  let ymax = r.f64()
  let bounds : Bounds = { xmin, ymin, xmax, ymax }
  bounds.validate()
  let zmin = r.f64()
  let zmax = r.f64()
  let mmin = r.f64()
  let mmax = r.f64()
  if !finite(zmin) || !finite(zmax) || !finite(mmin) || !finite(mmax) {
    raise InvalidData(68, "nonfinite range in header")
  }
  if kind.has_z() && zmin > zmax {
    raise InvalidData(68, "reversed Z range")
  }
  if kind.has_m() && mmin >= -1.0e38 && mmax >= -1.0e38 && mmin > mmax {
    raise InvalidData(84, "reversed M range")
  }
  { file_length, kind, bounds, zmin, zmax, mmin, mmax }
}

///|
fn write_header(
  length : Int,
  kind : ShapeType,
  bounds : Bounds?,
  zrange : (Double, Double)?,
  mrange : (Double, Double)?,
) -> Bytes {
  let w = ByteWriter::new()
  w.be32(9994)
  for _ in 0..<5 {
    w.be32(0)
  }
  w.be32(length / 2)
  w.le32(1000)
  w.le32(kind.code())
  match bounds {
    Some(b) => {
      w.f64(b.xmin)
      w.f64(b.ymin)
      w.f64(b.xmax)
      w.f64(b.ymax)
    }
    None =>
      for _ in 0..<4 {
        w.f64(0.0)
      }
  }
  match zrange {
    Some((lo, hi)) => {
      w.f64(lo)
      w.f64(hi)
    }
    None => {
      w.f64(0.0)
      w.f64(0.0)
    }
  }
  match mrange {
    Some((lo, hi)) => {
      w.f64(lo)
      w.f64(hi)
    }
    None => {
      let no_data = if kind.has_m() { -1.0e39 } else { 0.0 }
      w.f64(no_data)
      w.f64(no_data)
    }
  }
  w.bytes()
}