///|
pub(all) struct WriteOptions {
  index_node_size : Int32
  hilbert_sort : Bool
} derive(Eq, Debug, ToJson, @json.FromJson)

///|
pub fn WriteOptions::default() -> WriteOptions {
  { index_node_size: 16, hilbert_sort: true, }
}

///|
fn hilbert_key(x_ : Int32, y_ : Int32) -> Int64 {
  let mut x = x_
  let mut y = y_
  let mut distance = 0L
  let mut scale = 32768
  while scale > 0 {
    let rx = if (x & scale) != 0 { 1 } else { 0 }
    let ry = if (y & scale) != 0 { 1 } else { 0 }
    distance += scale.to_int64() * scale.to_int64() * ((3 * rx) ^ ry).to_int64()
    if ry == 0 {
      if rx == 1 {
        x = 65535 - x
        y = 65535 - y
      }
      let old_x = x
      x = y
      y = old_x
    }
    scale = scale >> 1
  }
  distance
}

///|
fn normalized_center(
  minimum : Double,
  maximum : Double,
  extent_min : Double,
  extent_max : Double,
) -> Int32 {
  // Half-scaled differences avoid overflowing on large finite coordinates.
  let width = extent_max / 2.0 - extent_min / 2.0
  if width <= 0.0 {
    return 0
  }
  let center = minimum / 4.0 + maximum / 4.0
  let value = (center - extent_min / 2.0) / width * 65535.0
  if value <= 0.0 {
    0
  } else if value >= 65535.0 {
    65535
  } else {
    value.to_int()
  }
}

///|
fn sorted_features(
  features : Array[Feature],
  extent : Bounds,
) -> Array[Feature] raise FgbError {
  let keys : Array[(Int64, Int32, Feature)] = []
  for i in 0.. {
    let order = a.0.compare(b.0)
    if order == 0 {
      a.1.compare(b.1)
    } else {
      order
    }
  })
  keys.map(item => item.2)
}

///|
fn build_index(
  features : Array[Feature],
  offsets : Array[Int32],
  node_size : Int32,
  limits : Limits,
) -> Bytes raise FgbError {
  let levels = index_levels(features.length(), node_size, limits~)
  let total = levels[0].end
  let empty : IndexNode = {
    bounds: { min_x: 0.0, min_y: 0.0, max_x: 0.0, max_y: 0.0, },
    offset: 0,
  }
  let nodes = Array::make(total, empty)
  for i in 0.. children.end - first {
        children.end
      } else {
        first + node_size
      }
      let mut bounds = nodes[first].bounds
      for child in (first + 1).. Bounds? {
  let mut bounds : Bounds? = None
  for feature in features {
    match feature.bounds() {
      None => ()
      Some(b) =>
        bounds = Some(
          match bounds {
            None => b
            Some(previous) => previous.union(b)
          },
        )
    }
  }
  bounds
}

///|
pub fn write_dataset(
  dataset : Dataset,
  options~ : WriteOptions = WriteOptions::default(),
  limits~ : Limits = Limits::default(),
) -> Bytes raise FgbError {
  limits.validate()
  if options.index_node_size < 0 ||
    options.index_node_size == 1 ||
    options.index_node_size > 65535 {
    raise InvalidArgument("invalid index node size")
  }
  enforce(dataset.features.length(), limits.max_features, "feature count")
  // Validate before computing bounds and Hilbert keys.
  for feature in dataset.features {
    match feature.geometry {
      None => ()
      Some(g) => g.validate(limits~)
    }
  }
  let envelope = dataset_bounds(dataset.features)
  let node_size = if dataset.features.is_empty() {
    0
  } else {
    options.index_node_size
  }
  let features = if node_size > 0 && options.hilbert_sort {
    match envelope {
      None => raise InvalidArgument("indexed file needs geometry bounds")
      Some(bounds) => sorted_features(dataset.features, bounds)
    }
  } else {
    dataset.features
  }
  let header = {
    ..dataset.header,
    features_count: features.length(),
    index_node_size: node_size,
    envelope,
  }
  let header_bytes = encode_header(header, limits~)
  let records : Array[Bytes] = []
  let offsets : Array[Int32] = []
  let mut feature_bytes = 0
  for feature in features {
    let record = encode_feature(feature, header, limits~)
    offsets.push(feature_bytes)
    feature_bytes = checked_add(feature_bytes, record.length())
    enforce(feature_bytes, limits.max_file_bytes, "feature section bytes")
    records.push(record)
  }
  let index = if node_size == 0 {
    b""
  } else {
    build_index(features, offsets, node_size, limits)
  }
  let total = checked_add(
    checked_add(8 + header_bytes.length(), index.length()),
    feature_bytes,
  )
  enforce(total, limits.max_file_bytes, "file bytes")
  let output = ByteWriter::new()
  output.bytes(b"\x66\x67\x62\x03\x66\x67\x62\x01")
  output.bytes(header_bytes)
  output.bytes(index)
  for record in records {
    output.bytes(record)
  }
  output.finish()
}