///|
pub(all) struct Record {
  number : Int
  offset : Int
  content_length : Int
  shape : Shape
} derive(Debug)

///|
pub(all) struct Shapefile {
  kind : ShapeType
  records : Array[Record]
  bounds : Bounds?
} derive(Debug)

///|
/// Cursor holds original bytes and decodes one record at a time.
pub struct ShpReader {
  data : Bytes
  header : Header
  limits : ReadLimits
  mut position : Int
  mut count : Int
}

///|
pub fn ShpReader::open(
  data : Bytes,
  limits? : ReadLimits = ReadLimits::default(),
) -> ShpReader raise ShapeError {
  limits.validate()
  if data.length() > limits.max_file_bytes {
    raise InvalidData(0, "file exceeds configured byte limit")
  }
  let header = read_header(data)
  { data, header, limits, position: 100, count: 0 }
}

///|
pub fn ShpReader::header(self : ShpReader) -> Header {
  self.header
}

///|
pub fn ShpReader::position(self : ShpReader) -> Int {
  self.position
}

///|
/// A failed decode does not advance the cursor.
pub fn ShpReader::next(self : ShpReader) -> Record? raise ShapeError {
  if self.position == self.data.length() {
    return None
  }
  if self.count >= self.limits.max_records {
    raise InvalidData(self.position, "record count exceeds configured limit")
  }
  let r : ByteReader = {
    data: self.data,
    pos: self.position,
    end: self.data.length(),
    base: 0,
  }
  let number = r.be32()
  if number != self.count + 1 {
    raise InvalidData(
      self.position,
      "record numbers must be consecutive starting at one",
    )
  }
  let content_length = words_to_bytes(r.be32(), self.position + 4)
  let body = r.take(content_length)
  let shape = decode_shape(body, self.position + 8, self.limits)
  if shape.kind != Null && shape.kind != self.header.kind {
    raise InvalidData(
      self.position + 8,
      "record shape type differs from file type",
    )
  }
  match shape.bounds() {
    Some(b) =>
      if b.xmin < self.header.bounds.xmin ||
        b.ymin < self.header.bounds.ymin ||
        b.xmax > self.header.bounds.xmax ||
        b.ymax > self.header.bounds.ymax {
        raise InvalidData(
          self.position + 8,
          "record geometry lies outside file bounds",
        )
      }
    None => ()
  }
  if shape.kind.has_z() {
    for p in shape.points {
      match p.z {
        Some(z) =>
          if z < self.header.zmin || z > self.header.zmax {
            raise InvalidData(self.position + 8, "Z outside file bounds")
          }
        None => ()
      }
    }
  }
  let record = { number, offset: self.position, content_length, shape }
  self.position += 8 + content_length
  self.count += 1
  Some(record)
}

///|
pub fn read_shp(
  data : Bytes,
  limits? : ReadLimits = ReadLimits::default(),
) -> Shapefile raise ShapeError {
  let reader = ShpReader::open(data, limits~)
  let records : Array[Record] = []
  let mut bounds : Bounds? = None
  while true {
    match reader.next() {
      None => break
      Some(record) => {
        bounds = merge_bounds(bounds, record.shape.bounds())
        records.push(record)
      }
    }
  }
  { kind: reader.header.kind, records, bounds }
}

///|
fn merge_bounds(a : Bounds?, b : Bounds?) -> Bounds? {
  match (a, b) {
    (None, b) => b
    (a, None) => a
    (Some(a), Some(b)) =>
      Some({
        xmin: @cmp.minimum(a.xmin, b.xmin),
        ymin: @cmp.minimum(a.ymin, b.ymin),
        xmax: @cmp.maximum(a.xmax, b.xmax),
        ymax: @cmp.maximum(a.ymax, b.ymax),
      })
  }
}

///|
fn read_bbox(r : ByteReader) -> Bounds raise ShapeError {
  let xmin = r.f64()
  let ymin = r.f64()
  let xmax = r.f64()
  let ymax = r.f64()
  let b : Bounds = { xmin, ymin, xmax, ymax }
  b.validate()
  b
}

///|
fn read_xy(r : ByteReader, count : Int) -> Array[Coordinate] raise ShapeError {
  ignore(checked_size(count, 16, r.remaining(), r.base + r.pos))
  let points : Array[Coordinate] = []
  for _ in 0.. Unit raise ShapeError {
  let lo = r.f64()
  let hi = r.f64()
  if !finite(lo) || !finite(hi) || lo > hi {
    raise InvalidData(r.base + r.pos - 16, "invalid Z range")
  }
  ignore(checked_size(points.length(), 8, r.remaining(), r.base + r.pos))
  for i in 0.. hi {
      raise InvalidData(
        r.base + r.pos - 8,
        "Z coordinate outside declared range",
      )
    }
    let p = points[i]
    points[i] = { x: p.x, y: p.y, z: Some(z), m: p.m }
  }
}

///|
fn measure(value : Double, offset : Int) -> Double? raise ShapeError {
  if !finite(value) {
    raise InvalidData(offset, "nonfinite measure")
  }
  if value < -1.0e38 {
    None
  } else {
    Some(value)
  }
}

///|
fn read_m_array(
  r : ByteReader,
  points : Array[Coordinate],
) -> Unit raise ShapeError {
  // ESRI permits the entire M range/array to be absent in multipart M/Z records.
  if r.remaining() == 0 {
    return
  }
  let lo = measure(r.f64(), r.base + r.pos - 8)
  let hi = measure(r.f64(), r.base + r.pos - 8)
  match (lo, hi) {
    (Some(lo), Some(hi)) =>
      if lo > hi {
        raise InvalidData(r.base + r.pos - 16, "reversed M range")
      }
    (None, None) => ()
    _ => raise InvalidData(r.base + r.pos - 16, "inconsistent no-data M range")
  }
  ignore(checked_size(points.length(), 8, r.remaining(), r.base + r.pos))
  for i in 0..
        if v < a || v > b {
          raise InvalidData(
            r.base + r.pos - 8,
            "measure outside declared range",
          )
        }
      (Some(_), _, _) =>
        raise InvalidData(r.base + r.pos - 8, "measure has no declared range")
      _ => ()
    }
    let p = points[i]
    points[i] = { x: p.x, y: p.y, z: p.z, m }
  }
}

///|
fn decode_shape(
  data : Bytes,
  base : Int,
  limits : ReadLimits,
) -> Shape raise ShapeError {
  let r = ByteReader::new(data, base~)
  let kind = shape_type(r.le32())
  let points : Array[Coordinate] = []
  let parts : Array[Int] = []
  let mut declared_bounds : Bounds? = None
  if kind.is_point() {
    if limits.max_points_per_record < 1 {
      raise InvalidData(base + r.pos, "point count exceeds configured limit")
    }
    let x = r.f64()
    let y = r.f64()
    let z = if kind.has_z() { Some(r.f64()) } else { None }
    let m = if kind.has_m() && r.remaining() > 0 {
      measure(r.f64(), base + r.pos - 8)
    } else {
      None
    }
    if kind == PointM && m is None && data.length() == 20 {
      raise InvalidData(base + 20, "PointM requires measure bytes")
    }
    points.push({ x, y, z, m })
  } else if kind != Null {
    declared_bounds = Some(read_bbox(r))
    let part_count = if kind.is_line() || kind.is_polygon() {
      r.le32()
    } else {
      0
    }
    let point_count = r.le32()
    if point_count < 0 || point_count > limits.max_points_per_record {
      raise InvalidData(
        base + r.pos - 4,
        "point count exceeds configured limit",
      )
    }
    if part_count < 0 || part_count > limits.max_parts_per_record {
      raise InvalidData(base + 36, "part count exceeds configured limit")
    }
    ignore(checked_size(part_count, 4, r.remaining(), base + r.pos))
    for _ in 0..
      if b.xmin < a.xmin ||
        b.ymin < a.ymin ||
        b.xmax > a.xmax ||
        b.ymax > a.ymax {
        raise InvalidData(base + 4, "geometry exceeds record bounds")
      }
    _ => ()
  }
  shape
}