// The two filter modules: a Bloom filter answers "definitely not, or probably yes" for the
// items it was built from, and a cuckoo filter answers the same while allowing deletes.
// Both report a member test as `1`/`0` under RESP2 and as `#t`/`#f` under RESP3, and both
// can dump themselves chunk by chunk for a backup.
// https://redis.io/docs/latest/commands/?group=bf
// https://redis.io/docs/latest/commands/?group=cf

///|
/// `BF.RESERVE key error_rate capacity [EXPANSION expansion] [NONSCALING]`: create a Bloom
/// filter sized for `capacity` items at `error_rate` false positives. A filter that grows
/// keeps its rate by adding sub-filters of `expansion` times the capacity; `nonscaling`
/// fixes it at the initial size and starts refusing items when full.
pub async fn Client::bf_reserve(
  self : Client,
  key : String,
  error_rate : Double,
  capacity : Int64,
  expansion? : Int64,
  nonscaling? : Bool,
) -> Unit raise RedisError {
  let args : Array[Bytes] = [
    b"BF.RESERVE",
    arg_string(key),
    arg_double(error_rate),
    arg_int64(capacity),
  ]
  push_some(args, b"EXPANSION", expansion.map(arg_int64))
  if nonscaling.unwrap_or(false) {
    args.push(b"NONSCALING")
  }
  status_ok(self.command(args))
}

///|
/// `BF.ADD key item`: add an item, returning whether the filter changed — false when it
/// probably held the item already.
pub async fn Client::bf_add(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"BF.ADD", arg_string(key), arg_string(item)]))
}

///|
/// `BF.MADD key item [item ...]`: add items, one flag each.
pub async fn Client::bf_madd(
  self : Client,
  key : String,
  items : Array[String],
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"BF.MADD", arg_string(key)]
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `BF.INSERT key [CAPACITY n] [ERROR rate] [EXPANSION n] [NOCREATE] [NONSCALING] ITEMS
/// item...`: add items, creating the filter from these parameters when it does not exist.
pub async fn Client::bf_insert(
  self : Client,
  key : String,
  items : Array[String],
  capacity? : Int64,
  error_rate? : Double,
  expansion? : Int64,
  nocreate? : Bool,
  nonscaling? : Bool,
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"BF.INSERT", arg_string(key)]
  push_some(args, b"CAPACITY", capacity.map(arg_int64))
  push_some(args, b"ERROR", error_rate.map(arg_double))
  push_some(args, b"EXPANSION", expansion.map(arg_int64))
  if nocreate.unwrap_or(false) {
    args.push(b"NOCREATE")
  }
  if nonscaling.unwrap_or(false) {
    args.push(b"NONSCALING")
  }
  args.push(b"ITEMS")
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `BF.EXISTS key item`: whether the filter probably holds the item.
pub async fn Client::bf_exists(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"BF.EXISTS", arg_string(key), arg_string(item)]))
}

///|
/// `BF.MEXISTS key item [item ...]`: one answer per item.
pub async fn Client::bf_mexists(
  self : Client,
  key : String,
  items : Array[String],
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"BF.MEXISTS", arg_string(key)]
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `BF.SCANDUMP key iterator`: the next chunk of a serialized filter, and the iterator to
/// ask for the one after it. Iteration ends when the iterator comes back as 0.
pub async fn Client::bf_scandump(
  self : Client,
  key : String,
  iterator : Int64,
) -> (Int64, Bytes?) raise RedisError {
  let parts = expect_array(
    self.command([b"BF.SCANDUMP", arg_string(key), arg_int64(iterator)]),
  )
  guard parts.length() >= 2 else {
    raise Protocol("unexpected BF.SCANDUMP reply")
  }
  (expect_int64(parts[0]), expect_bytes(parts[1]))
}

///|
/// `BF.LOADCHUNK key iterator data`: restore one chunk of a serialized filter.
pub async fn Client::bf_loadchunk(
  self : Client,
  key : String,
  iterator : Int64,
  data : Bytes,
) -> Unit raise RedisError {
  status_ok(
    self.command([b"BF.LOADCHUNK", arg_string(key), arg_int64(iterator), data]),
  )
}

///|
/// `BF.INFO key`: capacity, memory and item counts.
pub async fn Client::bf_info(
  self : Client,
  key : String,
) -> Info raise RedisError {
  decode_info(self.command([b"BF.INFO", arg_string(key)]))
}

///|
/// `BF.CARD key`: how many items were added, duplicates included.
pub async fn Client::bf_card(
  self : Client,
  key : String,
) -> Int64 raise RedisError {
  expect_int64(self.command([b"BF.CARD", arg_string(key)]))
}

///|
/// `CF.RESERVE key capacity [BUCKETSIZE n] [MAXITERATIONS n] [EXPANSION n]`: create a
/// cuckoo filter for `capacity` items.
pub async fn Client::cf_reserve(
  self : Client,
  key : String,
  capacity : Int64,
  bucket_size? : Int64,
  max_iterations? : Int64,
  expansion? : Int64,
) -> Unit raise RedisError {
  let args : Array[Bytes] = [
    b"CF.RESERVE",
    arg_string(key),
    arg_int64(capacity),
  ]
  push_some(args, b"BUCKETSIZE", bucket_size.map(arg_int64))
  push_some(args, b"MAXITERATIONS", max_iterations.map(arg_int64))
  push_some(args, b"EXPANSION", expansion.map(arg_int64))
  status_ok(self.command(args))
}

///|
/// `CF.ADD key item`: add an item, returning whether it was added.
pub async fn Client::cf_add(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"CF.ADD", arg_string(key), arg_string(item)]))
}

///|
/// `CF.ADDNX key item`: add an item only if the filter does not hold it yet.
pub async fn Client::cf_addnx(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"CF.ADDNX", arg_string(key), arg_string(item)]))
}

///|
/// `CF.INSERT key [CAPACITY n] [NOCREATE] ITEMS item...`: add items, creating the filter
/// when it does not exist.
pub async fn Client::cf_insert(
  self : Client,
  key : String,
  items : Array[String],
  capacity? : Int64,
  nocreate? : Bool,
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"CF.INSERT", arg_string(key)]
  push_some(args, b"CAPACITY", capacity.map(arg_int64))
  if nocreate.unwrap_or(false) {
    args.push(b"NOCREATE")
  }
  args.push(b"ITEMS")
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `CF.INSERTNX key [CAPACITY n] [NOCREATE] ITEMS item...`: add the items the filter does
/// not hold yet.
pub async fn Client::cf_insertnx(
  self : Client,
  key : String,
  items : Array[String],
  capacity? : Int64,
  nocreate? : Bool,
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"CF.INSERTNX", arg_string(key)]
  push_some(args, b"CAPACITY", capacity.map(arg_int64))
  if nocreate.unwrap_or(false) {
    args.push(b"NOCREATE")
  }
  args.push(b"ITEMS")
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `CF.EXISTS key item`: whether the filter probably holds the item.
pub async fn Client::cf_exists(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"CF.EXISTS", arg_string(key), arg_string(item)]))
}

///|
/// `CF.MEXISTS key item [item ...]`: one answer per item.
pub async fn Client::cf_mexists(
  self : Client,
  key : String,
  items : Array[String],
) -> Array[Bool] raise RedisError {
  let args : Array[Bytes] = [b"CF.MEXISTS", arg_string(key)]
  for item in items {
    args.push(arg_string(item))
  }
  expect_array(self.command(args)).map(expect_bool)
}

///|
/// `CF.DEL key item`: remove one occurrence of an item. True if it was there.
pub async fn Client::cf_del(
  self : Client,
  key : String,
  item : String,
) -> Bool raise RedisError {
  expect_bool(self.command([b"CF.DEL", arg_string(key), arg_string(item)]))
}

///|
/// `CF.COUNT key item`: how many times the item was added.
pub async fn Client::cf_count(
  self : Client,
  key : String,
  item : String,
) -> Int64 raise RedisError {
  expect_int64(self.command([b"CF.COUNT", arg_string(key), arg_string(item)]))
}

///|
/// `CF.SCANDUMP key iterator`: the next chunk of a serialized filter, and the iterator for
/// the one after it.
pub async fn Client::cf_scandump(
  self : Client,
  key : String,
  iterator : Int64,
) -> (Int64, Bytes?) raise RedisError {
  let parts = expect_array(
    self.command([b"CF.SCANDUMP", arg_string(key), arg_int64(iterator)]),
  )
  guard parts.length() >= 2 else {
    raise Protocol("unexpected CF.SCANDUMP reply")
  }
  (expect_int64(parts[0]), expect_bytes(parts[1]))
}

///|
/// `CF.LOADCHUNK key iterator data`: restore one chunk of a serialized filter.
pub async fn Client::cf_loadchunk(
  self : Client,
  key : String,
  iterator : Int64,
  data : Bytes,
) -> Unit raise RedisError {
  status_ok(
    self.command([b"CF.LOADCHUNK", arg_string(key), arg_int64(iterator), data]),
  )
}

///|
/// `CF.INFO key`: size, buckets and item counts.
pub async fn Client::cf_info(
  self : Client,
  key : String,
) -> Info raise RedisError {
  decode_info(self.command([b"CF.INFO", arg_string(key)]))
}