///|
/// Matching SHP and SHX byte streams, ready for filesystem or network storage.
pub(all) struct ShapeFiles {
  shp : Bytes
  shx : Bytes
} derive(Debug)

///|
fn bounds_bytes(w : ByteWriter, bounds : Bounds?) -> Unit {
  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)
      }
  }
}

///|
fn extend_range(
  range : (Double, Double)?,
  value : Double?,
) -> (Double, Double)? {
  match (range, value) {
    (r, None) => r
    (None, Some(v)) => Some((v, v))
    (Some((a, b)), Some(v)) =>
      Some((if v < a { v } else { a }, if v > b { v } else { b }))
  }
}

///|
fn ordinate_range(points : Array[Coordinate], z : Bool) -> (Double, Double)? {
  let mut range = None
  for p in points {
    range = extend_range(range, if z { p.z } else { p.m })
  }
  range
}

///|
fn write_ordinates(
  w : ByteWriter,
  points : Array[Coordinate],
  z : Bool,
) -> Unit {
  let range = ordinate_range(points, z)
  match range {
    Some((lo, hi)) => {
      w.f64(lo)
      w.f64(hi)
    }
    None => {
      w.f64(-1.0e39)
      w.f64(-1.0e39)
    }
  }
  for p in points {
    w.f64((if z { p.z } else { p.m }).unwrap_or(-1.0e39))
  }
}

///|
fn encode_shape(shape : Shape) -> Bytes raise ShapeError {
  shape.validate()
  let w = ByteWriter::new()
  w.le32(shape.kind.code())
  if shape.kind == Null {
    return w.bytes()
  }
  if shape.kind.is_point() {
    let p = shape.points[0]
    w.f64(p.x)
    w.f64(p.y)
    if shape.kind.has_z() {
      w.f64(p.z.unwrap())
    }
    if shape.kind.has_m() {
      w.f64(p.m.unwrap_or(-1.0e39))
    }
  } else {
    bounds_bytes(w, shape.bounds())
    if shape.kind.is_line() || shape.kind.is_polygon() {
      w.le32(shape.parts.length())
    }
    w.le32(shape.points.length())
    for part in shape.parts {
      w.le32(part)
    }
    for p in shape.points {
      w.f64(p.x)
      w.f64(p.y)
    }
    if shape.kind.has_z() {
      write_ordinates(w, shape.points, true)
    }
    if shape.kind.has_m() {
      write_ordinates(w, shape.points, false)
    }
  }
  w.bytes()
}

///|
/// Accumulates encoded records; append validates a record before mutation.
pub struct ShpWriter {
  kind : ShapeType
  limits : ReadLimits
  records : Array[Bytes]
  mut total_bytes : Int
  mut bounds : Bounds?
  mut zrange : (Double, Double)?
  mut mrange : (Double, Double)?
}

///|
pub fn ShpWriter::new(
  kind : ShapeType,
  limits? : ReadLimits = ReadLimits::default(),
) -> ShpWriter raise ShapeError {
  limits.validate()
  {
    kind,
    limits,
    records: [],
    total_bytes: 100,
    bounds: None,
    zrange: None,
    mrange: None,
  }
}

///|
pub fn ShpWriter::record_count(self : ShpWriter) -> Int {
  self.records.length()
}

///|
pub fn ShpWriter::append(
  self : ShpWriter,
  shape : Shape,
) -> Unit raise ShapeError {
  if shape.kind != Null && shape.kind != self.kind {
    raise InvalidData(
      self.records.length(),
      "shape type differs from writer type",
    )
  }
  if self.records.length() >= self.limits.max_records {
    raise InvalidData(self.records.length(), "writer record limit exceeded")
  }
  if shape.points.length() > self.limits.max_points_per_record ||
    shape.parts.length() > self.limits.max_parts_per_record {
    raise InvalidData(self.records.length(), "writer geometry limit exceeded")
  }
  // Bound the eventual body size before encoding arrays.
  let point_width = if shape.kind.has_z() {
    32
  } else if shape.kind.has_m() {
    24
  } else {
    16
  }
  let available = self.limits.max_file_bytes - self.total_bytes
  if available < 12 ||
    shape.points.length() > available / point_width ||
    shape.parts.length() > available / 4 {
    raise InvalidData(self.total_bytes, "writer byte limit exceeded")
  }
  let body = encode_shape(shape)
  if body.length() > available - 8 {
    raise InvalidData(self.total_bytes, "writer byte limit exceeded")
  }
  self.records.push(body)
  self.total_bytes += 8 + body.length()
  self.bounds = merge_bounds(self.bounds, shape.bounds())
  for p in shape.points {
    self.zrange = extend_range(self.zrange, p.z)
    self.mrange = extend_range(self.mrange, p.m)
  }
}

///|
/// Finishing is repeatable and does not consume the writer.
pub fn ShpWriter::finish(self : ShpWriter) -> ShapeFiles {
  let shp = ByteWriter::new()
  let shx = ByteWriter::new()
  shp.append(
    write_header(
      self.total_bytes,
      self.kind,
      self.bounds,
      self.zrange,
      self.mrange,
    ),
  )
  shx.append(
    write_header(
      100 + self.records.length() * 8,
      self.kind,
      self.bounds,
      self.zrange,
      self.mrange,
    ),
  )
  let mut offset = 100
  for i in 0.. ShapeFiles raise ShapeError {
  let writer = ShpWriter::new(kind, limits~)
  for shape in shapes {
    writer.append(shape)
  }
  writer.finish()
}