///|
/// A handle to the Coordinate API: network coordinates (Vivaldi-embedded
/// positions) for latency-aware routing and nearest-node selection.
pub struct Coordinate {
  client : Client
}

///|
/// A single node's network coordinate: a `vec` in the embedded space plus an
/// `error`, `adjustment` and `height`.
pub(all) struct CoordinateValue {
  vec : Array[Double]
  error : Double
  adjustment : Double
  height : Double
}

///|
/// One node's coordinate in a segment.
pub(all) struct CoordinateEntry {
  node : String
  segment : String?
  coord : CoordinateValue?
}

///|
/// A node and the coordinates it holds for each datacenter segment.
pub(all) struct CoordinateNode {
  node : String
  coordinates : Array[CoordinateEntry]
}

///|
/// A `CoordinateValue` from its JSON object.
fn coordinate_value_of(j : Json) -> CoordinateValue {
  let m = json_map(j)
  let vec : Array[Double] = []
  for item in jarray(m, "Vec") {
    if item is Number(n, ..) {
      vec.push(n)
    }
  }
  {
    vec,
    error: match jdouble_opt(m, "Error") {
      Some(d) => d
      None => 0.0
    },
    adjustment: match jdouble_opt(m, "Adjustment") {
      Some(d) => d
      None => 0.0
    },
    height: match jdouble_opt(m, "Height") {
      Some(d) => d
      None => 0.0
    },
  }
}

///|
/// A `CoordinateValue` as a JSON object.
fn coordinate_value_to_json(v : CoordinateValue) -> Json {
  let m = jobj()
  let vec : Array[Json] = []
  for d in v.vec {
    vec.push(@json.to_json(d))
  }
  m["Vec"] = @json.to_json(vec)
  m["Error"] = @json.to_json(v.error)
  m["Adjustment"] = @json.to_json(v.adjustment)
  m["Height"] = @json.to_json(v.height)
  @json.to_json(m)
}

///|
/// A `CoordinateEntry` from its JSON object.
fn coordinate_entry_of(j : Json) -> CoordinateEntry {
  let m = json_map(j)
  {
    node: jstr(m, "Node"),
    segment: jstr_opt(m, "Segment"),
    coord: match m.get("Coord") {
      Some(c) =>
        if c is Object(_) {
          Some(coordinate_value_of(c))
        } else {
          None
        }
      None => None
    },
  }
}

///|
/// A `CoordinateNode` from its JSON object.
fn coordinate_node_of(j : Json) -> CoordinateNode {
  let m = json_map(j)
  let coordinates : Array[CoordinateEntry] = []
  for item in jarray(m, "Coordinates") {
    coordinates.push(coordinate_entry_of(item))
  }
  { node: jstr(m, "Node"), coordinates, }
}

///|
/// The coordinates of every datacenter, as seen from this node.
pub async fn Coordinate::datacenters(
  self : Coordinate,
  options~ : QueryOptions = default_query_options,
) -> (Array[CoordinateNode], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/coordinate/datacenters", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[CoordinateNode] = []
  for item in json_array(json) {
    out.push(coordinate_node_of(item))
  }
  (out, meta)
}

///|
/// The coordinates of the nodes in this datacenter.
pub async fn Coordinate::nodes(
  self : Coordinate,
  options~ : QueryOptions = default_query_options,
) -> (Array[CoordinateEntry], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/coordinate/nodes", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[CoordinateEntry] = []
  for item in json_array(json) {
    out.push(coordinate_entry_of(item))
  }
  (out, meta)
}

///|
/// Publish this node's own `coord`.
pub async fn Coordinate::update(
  self : Coordinate,
  coord : CoordinateValue,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let m = jobj()
  m["Coord"] = coordinate_value_to_json(coord)
  let body = @utf8.encode(@json.to_json(m).stringify())
  let (_, meta) = self.client.do_put("/v1/coordinate/update", body, options, [])
  meta
}