// The geo commands, named as in redis-py. These index points by longitude/latitude in a
// sorted set (via the Geohash score) and query them; `geosearch` is the modern, unified
// query that replaces the older `GEORADIUS`/`GEORADIUSBYMEMBER`.

///|
/// `GEOADD key longitude latitude member...`: index the members, returning how many were
/// new.
pub async fn Client::geoadd(
  self : Client,
  key : String,
  points : Array[(String, Double, Double)],
) -> Int64 raise RedisError {
  let args : Array[Bytes] = [b"GEOADD", arg_string(key)]
  for (m, lon, lat) in points {
    args.push(arg_double(lon))
    args.push(arg_double(lat))
    args.push(arg_string(m))
  }
  expect_int64(self.command(args))
}

///|
/// `GEODIST key a b [unit]`: the distance between members `a` and `b`, or `None` if a
/// member is absent. `unit` is one of `m`, `km`, `mi`, `ft` (metres by default).
pub async fn Client::geodist(
  self : Client,
  key : String,
  a : String,
  b : String,
  unit? : String,
) -> Double? raise RedisError {
  let args : Array[Bytes] = [
    b"GEODIST",
    arg_string(key),
    arg_string(a),
    arg_string(b),
  ]
  match unit {
    Some(u) => args.push(arg_string(u))
    None => ()
  }
  match expect_bytes(self.command(args)) {
    Some(b) =>
      Some(
        @string.parse_double(@utf8.decode_lossy(b[:])) catch {
          _ => raise Protocol("invalid geodist reply")
        },
      )
    None => None
  }
}

///|
/// `GEOHASH key member...`: the geohash string of each member, `None` where absent.
pub async fn Client::geohash(
  self : Client,
  key : String,
  members : Array[String],
) -> Array[String?] raise RedisError {
  let args : Array[Bytes] = [b"GEOHASH", arg_string(key)]
  for m in members {
    args.push(arg_string(m))
  }
  let items = expect_array(self.command(args))
  items.map(x => expect_string(x))
}

///|
/// `GEOPOS key member...`: the `[longitude, latitude]` of each member, `None` where
/// absent.
pub async fn Client::geopos(
  self : Client,
  key : String,
  members : Array[String],
) -> Array[(Double, Double)?] raise RedisError {
  let args : Array[Bytes] = [b"GEOPOS", arg_string(key)]
  for m in members {
    args.push(arg_string(m))
  }
  let items = expect_array(self.command(args))
  items.map(x => {
    match x {
      @resp.Null => None
      @resp.Array(pair) => {
        guard pair.length() == 2 else {
          raise Protocol("unexpected geopos reply")
        }
        let lon = expect_double(pair[0])
        let lat = expect_double(pair[1])
        Some((lon, lat))
      }
      _ => raise Protocol("unexpected geopos reply")
    }
  })
}

///|
/// `GEOSEARCH key [FROMMEMBER m] [FROMLONLAT lon lat] [BYRADIUS r unit] [BYBOX w h unit]
/// [COUNT n] [ASC|DESC]`: the members matching a circular or rectangular area, optionally
/// ordered and capped. Returns the member names.
pub async fn Client::geosearch(
  self : Client,
  key : String,
  from_member? : String,
  longitude? : Double,
  latitude? : Double,
  radius? : Double,
  width? : Double,
  height? : Double,
  unit : String,
  count? : Int32,
  asc? : Bool,
  desc? : Bool,
) -> Array[String] raise RedisError {
  let args : Array[Bytes] = [b"GEOSEARCH", arg_string(key)]
  match from_member {
    Some(m) => {
      args.push(b"FROMMEMBER")
      args.push(arg_string(m))
    }
    None => ()
  }
  match (longitude, latitude) {
    (Some(lon), Some(lat)) => {
      args.push(b"FROMLONLAT")
      args.push(arg_double(lon))
      args.push(arg_double(lat))
    }
    _ => ()
  }
  match (radius, width, height) {
    (Some(r), _, _) => {
      args.push(b"BYRADIUS")
      args.push(arg_double(r))
    }
    (_, Some(w), Some(h)) => {
      args.push(b"BYBOX")
      args.push(arg_double(w))
      args.push(arg_double(h))
    }
    _ => ()
  }
  args.push(arg_string(unit))
  push_some(args, b"COUNT", count.map(arg_int))
  if asc.unwrap_or(false) {
    args.push(b"ASC")
  }
  if desc.unwrap_or(false) {
    args.push(b"DESC")
  }
  let items = expect_array(self.command(args))
  items.map(x => @utf8.decode_lossy(expect_bytes(x).unwrap()))
}