use yo_common::num::parse_f64;
use yo_common::{Code, Error, Result};
use yo_kv::geo::{self, Kind, Shape, Unit};
use yo_kv::geos::{self, Limit, Scratch, Sort};
use yo_kv::{Db, Gate, Keyspace, ZAdd};
use super::args::{self, Args, is};
use super::table::Spec;
use crate::reply::Out;
const BAD_UNIT: &str = "unsupported unit provided. please use M, KM, FT, MI";
const COUNT_POSITIVE: &str = "COUNT must be > 0";
const ANY_NEEDS_COUNT: &str = "the ANY argument requires COUNT argument";
const NUMERIC_RADIUS: &str = "need numeric radius";
const NEGATIVE_RADIUS: &str = "radius cannot be negative";
const NUMERIC_WIDTH: &str = "need numeric width";
const NUMERIC_HEIGHT: &str = "need numeric height";
const NEGATIVE_BOX: &str = "height or width cannot be negative";
const FROM_ONE: &str = "FROMMEMBER or FROMLONLAT";
const BY_ONE: &str = "BYRADIUS and BYBOX";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Centre {
Coords,
Member,
Options,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Store {
No,
Option,
Argument,
}
#[derive(Debug, Clone, Copy)]
struct Form {
src: usize,
base: usize,
centre: Centre,
store: Store,
}
impl Form {
fn of(name: &str) -> Form {
match name {
"georadius" => Form {
src: 1,
base: 6,
centre: Centre::Coords,
store: Store::Option,
},
"georadius_ro" => Form {
src: 1,
base: 6,
centre: Centre::Coords,
store: Store::No,
},
"georadiusbymember" => Form {
src: 1,
base: 5,
centre: Centre::Member,
store: Store::Option,
},
"georadiusbymember_ro" => Form {
src: 1,
base: 5,
centre: Centre::Member,
store: Store::No,
},
"geosearchstore" => Form {
src: 2,
base: 3,
centre: Centre::Options,
store: Store::Argument,
},
_ => Form {
src: 1,
base: 2,
centre: Centre::Options,
store: Store::No,
},
}
}
}
pub(super) fn execute(db: &Db, spec: &Spec, args: Args<'_>, out: &mut Out) -> Result<()> {
let key = args.get(1);
match spec.name {
"geoadd" => add(&mut db.hold(key), args, out),
"geopos" => pos(&mut db.hold(key), args, out),
"geohash" => hash(&mut db.hold(key), args, out),
"geodist" => dist(&mut db.hold(key), args, out),
_ => search(db, spec, args, out),
}
}
fn add(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let (mut nx, mut xx) = (false, false);
let mut opts = ZAdd::default();
let mut at = 2;
while at < args.len() {
let arg = args.get(at);
if is(arg, b"nx") {
nx = true;
} else if is(arg, b"xx") {
xx = true;
} else if is(arg, b"ch") {
opts.changed = true;
} else {
break;
}
at += 1;
}
let left = args.len() - at;
if left == 0 || !left.is_multiple_of(3) || (nx && xx) {
return Err(args::syntax());
}
opts.gate = if nx {
Gate::IfMissing
} else if xx {
Gate::IfPresent
} else {
Gate::Always
};
for i in (at..args.len()).step_by(3) {
args.float(i)?;
args.float(i + 1)?;
}
let points = (at..args.len()).step_by(3).map(|i| {
(
parse_f64(args.get(i)).expect("checked"),
parse_f64(args.get(i + 1)).expect("checked"),
args.get(i + 2),
)
});
out.uint(db.geoadd(args.get(1), points, opts)? as u64);
Ok(())
}
fn pos(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
out.array(args.len() - 2);
let members = (2..args.len()).map(|i| args.get(i));
db.geopos(args.get(1), members, |found| match found {
Some((lon, lat)) => {
out.array(2);
out.double(lon);
out.double(lat);
}
None => out.nil_array(),
})
}
fn hash(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
out.array(args.len() - 2);
let members = (2..args.len()).map(|i| args.get(i));
db.geohash(args.get(1), members, |found| match found {
Some(text) => out.bulk(text),
None => out.nil(),
})
}
fn dist(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
if args.len() > 5 {
return Err(args::syntax());
}
let unit = match args.opt(4) {
Some(word) => parse_unit(word)?,
None => Unit::M,
};
match db.geodist(args.get(1), args.get(2), args.get(3))? {
Some(metres) => out.distance(metres / unit.metres()),
None => out.nil(),
}
Ok(())
}
fn search(db: &Db, spec: &Spec, args: Args<'_>, out: &mut Out) -> Result<()> {
let form = Form::of(spec.name);
let key = args.get(form.src);
let here = db.hold(key).zcard(key)? != 0;
let mut dest = (form.store == Store::Argument).then(|| args.get(1));
let mut storedist = false;
let (mut lon, mut lat) = (0.0, 0.0);
let mut from = None;
let mut kind = Kind::Circle { radius: 0.0 };
let mut unit = Unit::M;
match form.centre {
Centre::Coords => {
(lon, lat) = coords(args, 2)?;
(kind, unit) = radius(args, 4)?;
}
Centre::Member => {
from = Some(args.get(2));
if here {
(lon, lat) = centre(db, key, args.get(2))?;
}
(kind, unit) = radius(args, 3)?;
}
Centre::Options => {}
}
let (mut withdist, mut withhash, mut withcoord) = (false, false, false);
let (mut from_member, mut from_lonlat) = (false, false);
let (mut by_radius, mut by_box) = (false, false);
let mut sort = None;
let mut count = None;
let mut any = false;
let options = form.centre == Centre::Options;
let mut at = form.base;
while at < args.len() {
let arg = args.get(at);
let after = args.len() - at - 1;
if is(arg, b"withdist") {
withdist = true;
} else if is(arg, b"withhash") {
withhash = true;
} else if is(arg, b"withcoord") {
withcoord = true;
} else if is(arg, b"any") {
any = true;
} else if is(arg, b"asc") {
sort = Some(Sort::Near);
} else if is(arg, b"desc") {
sort = Some(Sort::Far);
} else if is(arg, b"count") && after >= 1 {
let want = args.int(at + 1)?;
if want <= 0 {
return Err(plain(COUNT_POSITIVE));
}
count = Some(want as usize);
at += 1;
} else if (is(arg, b"store") || is(arg, b"storedist"))
&& after >= 1
&& form.store == Store::Option
{
dest = Some(args.get(at + 1));
storedist = is(arg, b"storedist");
at += 1;
} else if is(arg, b"storedist") && form.store == Store::Argument {
storedist = true;
} else if is(arg, b"frommember") && after >= 1 && options && !from_lonlat {
from = Some(args.get(at + 1));
if here {
(lon, lat) = centre(db, key, args.get(at + 1))?;
}
from_member = true;
at += 1;
} else if is(arg, b"fromlonlat") && after >= 2 && options && !from_member {
(lon, lat) = coords(args, at + 1)?;
from_lonlat = true;
at += 2;
} else if is(arg, b"byradius") && after >= 2 && options && !by_box {
(kind, unit) = radius(args, at + 1)?;
by_radius = true;
at += 2;
} else if is(arg, b"bybox") && after >= 3 && options && !by_radius {
(kind, unit) = rectangle(args, at + 1)?;
by_box = true;
at += 3;
} else {
return Err(args::syntax());
}
at += 1;
}
if dest.is_some() && (withdist || withhash || withcoord) {
return Err(store_clash(form.store == Store::Argument));
}
if options && !(from_member || from_lonlat) {
return Err(exactly_one(FROM_ONE, args.name()));
}
if options && !(by_radius || by_box) {
return Err(exactly_one(BY_ONE, args.name()));
}
if any && count.is_none() {
return Err(plain(ANY_NEEDS_COUNT));
}
let mut shape = Shape {
lon,
lat,
kind,
unit,
};
let limit = Limit { sort, count, any };
match dest {
Some(into) => {
out.uint(db.geosearchstore(into, key, from, &shape, limit, storedist)? as u64);
}
None => {
let mut stripe = db.hold(key);
if let Some(member) = from
&& let Some(centre) = stripe.geocentre(key, member)?
{
(shape.lon, shape.lat) = centre;
}
stripe.geosearch(key, &shape, limit)?;
found(stripe.geohits(), unit, [withdist, withhash, withcoord], out);
}
}
Ok(())
}
fn found(hits: &Scratch, unit: Unit, with: [bool; 3], out: &mut Out) {
let [dist, hash, coord] = with;
let extra = usize::from(dist) + usize::from(hash) + usize::from(coord);
out.array(hits.len());
for (member, hit) in hits.iter() {
if extra != 0 {
out.array(extra + 1);
}
out.bulk(member);
if dist {
out.distance(hit.metres / unit.metres());
}
if hash {
out.int(hit.score as i64);
}
if coord {
out.array(2);
out.double(hit.lon);
out.double(hit.lat);
}
}
}
fn coords(args: Args<'_>, at: usize) -> Result<(f64, f64)> {
let lon = args.float(at)?;
let lat = args.float(at + 1)?;
if !geo::in_range(lon, lat) {
return Err(geos::out_of_range(lon, lat));
}
Ok((lon, lat))
}
fn radius(args: Args<'_>, at: usize) -> Result<(Kind, Unit)> {
let radius = parse_f64(args.get(at)).ok_or_else(|| plain(NUMERIC_RADIUS))?;
if radius < 0.0 {
return Err(plain(NEGATIVE_RADIUS));
}
Ok((Kind::Circle { radius }, parse_unit(args.get(at + 1))?))
}
fn rectangle(args: Args<'_>, at: usize) -> Result<(Kind, Unit)> {
let width = parse_f64(args.get(at)).ok_or_else(|| plain(NUMERIC_WIDTH))?;
let height = parse_f64(args.get(at + 1)).ok_or_else(|| plain(NUMERIC_HEIGHT))?;
if width < 0.0 || height < 0.0 {
return Err(plain(NEGATIVE_BOX));
}
Ok((Kind::Rect { width, height }, parse_unit(args.get(at + 2))?))
}
fn centre(db: &Db, key: &[u8], member: &[u8]) -> Result<(f64, f64)> {
db.hold(key)
.geocentre(key, member)?
.ok_or_else(geos::no_member)
}
fn parse_unit(word: &[u8]) -> Result<Unit> {
Unit::parse(word).ok_or_else(|| plain(BAD_UNIT))
}
fn plain(msg: &'static str) -> Error {
Error::new(Code::Invalid, msg)
}
fn store_clash(named: bool) -> Error {
let who = if named {
"GEOSEARCHSTORE"
} else {
"STORE option in GEORADIUS"
};
Error::fmt(
Code::Invalid,
format_args!("{who} is not compatible with WITHDIST, WITHHASH and WITHCOORD options"),
)
}
fn exactly_one(which: &str, name: &[u8]) -> Error {
yo_alloc::allow(|| {
Error::fmt(
Code::Invalid,
format_args!(
"exactly one of {which} can be specified for {}",
String::from_utf8_lossy(name)
),
)
})
}