//! A strict reader of JSON text (RFC 8259) for the documents the tensor-file and scene
//! mappings read: `extensions/prior-carriage.md` §9 (a safetensors header) and
//! `extensions/scene-carriage.md` §3 (a glTF document); for the integer decoders' parameter
//! sets, `entropy.json` and a `WMB1` bundle's tables (`extensions/integer-payloads.md` §9 and
//! §14), whose integers are read from their spellings; and for the splat export's JSON
//! bitstream, whose numbers [`crate::int_splat`] reads in integer arithmetic. No number is
//! converted to a float on the way in, so the integer decode paths read their JSON with no
//! floating point.
//!
//! It keeps what a lenient parser loses: members in the order they were written, a
//! member named twice (refused), and each number's spelling (a number is never rounded on
//! the way in; the caller reads the spelling as it needs). Nesting is limited to
//! [`MAX_DEPTH`] levels. Invalid UTF-8, an unpaired surrogate escape, a control character
//! in a string and anything after the value are refused.

/// The most levels of nesting a document may have.
///
/// cbindgen:ignore
pub const MAX_DEPTH: usize = 64;

/// A JSON value with its members in written order and its numbers as spelled.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Json {
    Null,
    Bool(bool),
    /// The number's spelling, as RFC 8259 §6 admits it.
    Num(String),
    Str(String),
    Arr(Vec<Json>),
    Obj(Vec<(String, Json)>),
}

impl Json {
    /// The member `k` of an object.
    pub fn get(&self, k: &str) -> Option<&Json> {
        match self {
            Json::Obj(m) => m.iter().find(|(n, _)| n == k).map(|(_, v)| v),
            _ => None,
        }
    }
    #[cfg_attr(not(any(feature = "prior_wire", feature = "scene_map", feature = "conform_result", feature = "fleet")), allow(dead_code))]
    pub fn as_str(&self) -> Option<&str> {
        match self {
            Json::Str(s) => Some(s),
            _ => None,
        }
    }
    #[cfg_attr(not(any(feature = "prior_wire", feature = "scene_map", feature = "neural_int")), allow(dead_code))]
    pub fn as_arr(&self) -> Option<&[Json]> {
        match self {
            Json::Arr(a) => Some(a),
            _ => None,
        }
    }
    #[cfg_attr(not(any(feature = "prior_wire", feature = "scene_map", feature = "conform_result", feature = "fleet")), allow(dead_code))]
    pub fn as_obj(&self) -> Option<&[(String, Json)]> {
        match self {
            Json::Obj(m) => Some(m),
            _ => None,
        }
    }
    /// A number spelled as a plain non-negative decimal integer (no sign, fraction or
    /// exponent, no leading zero) that fits `u64`.
    #[cfg_attr(
        not(any(feature = "prior_wire", feature = "scene_map", feature = "conform_result", feature = "fleet", feature = "video_receipt", feature = "channel_lineup", feature = "splat_int")),
        allow(dead_code)
    )]
    pub fn as_u64(&self) -> Option<u64> {
        match self {
            Json::Num(s) if is_plain_uint(s) => s.parse().ok(),
            _ => None,
        }
    }
    /// A number spelled as a decimal integer (an optional `-`, then a plain non-negative
    /// integer: no fraction or exponent) that fits `i64`. `-0` is zero.
    #[cfg_attr(not(feature = "neural_int"), allow(dead_code))]
    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Json::Num(s) if is_plain_uint(s.strip_prefix('-').unwrap_or(s)) => s.parse().ok(),
            _ => None,
        }
    }
}

fn is_plain_uint(s: &str) -> bool {
    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) && (s == "0" || !s.starts_with('0'))
}

/// Read `bytes` as one JSON text. The error says where and why.
pub fn read(bytes: &[u8]) -> Result<Json, String> {
    let text = std::str::from_utf8(bytes).map_err(|e| format!("not UTF-8 at byte {}", e.valid_up_to()))?;
    let mut p = P { b: text.as_bytes(), i: 0 };
    p.ws();
    let v = p.value(0)?;
    p.ws();
    if p.i != p.b.len() {
        return Err(format!("bytes follow the value at byte {}", p.i));
    }
    Ok(v)
}

struct P<'a> {
    b: &'a [u8],
    i: usize,
}

impl P<'_> {
    fn ws(&mut self) {
        while let Some(&c) = self.b.get(self.i) {
            if matches!(c, b' ' | b'\t' | b'\n' | b'\r') {
                self.i += 1;
            } else {
                break;
            }
        }
    }
    fn err<T>(&self, why: &str) -> Result<T, String> {
        Err(format!("{why} at byte {}", self.i))
    }
    fn lit(&mut self, s: &str, v: Json) -> Result<Json, String> {
        if self.b[self.i..].starts_with(s.as_bytes()) {
            self.i += s.len();
            Ok(v)
        } else {
            self.err("not a JSON value")
        }
    }
    fn value(&mut self, depth: usize) -> Result<Json, String> {
        if depth >= MAX_DEPTH {
            return self.err("nested too deeply");
        }
        match self.b.get(self.i) {
            None => self.err("the text ends where a value is due"),
            Some(b'{') => {
                self.i += 1;
                let mut m: Vec<(String, Json)> = Vec::new();
                self.ws();
                if self.b.get(self.i) == Some(&b'}') {
                    self.i += 1;
                    return Ok(Json::Obj(m));
                }
                loop {
                    self.ws();
                    if self.b.get(self.i) != Some(&b'"') {
                        return self.err("a member name is due");
                    }
                    let k = self.string()?;
                    if m.iter().any(|(n, _)| *n == k) {
                        return self.err(&format!("member {k:?} named twice"));
                    }
                    self.ws();
                    if self.b.get(self.i) != Some(&b':') {
                        return self.err("':' is due");
                    }
                    self.i += 1;
                    self.ws();
                    let v = self.value(depth + 1)?;
                    m.push((k, v));
                    self.ws();
                    match self.b.get(self.i) {
                        Some(b',') => self.i += 1,
                        Some(b'}') => {
                            self.i += 1;
                            return Ok(Json::Obj(m));
                        }
                        _ => return self.err("',' or '}' is due"),
                    }
                }
            }
            Some(b'[') => {
                self.i += 1;
                let mut a = Vec::new();
                self.ws();
                if self.b.get(self.i) == Some(&b']') {
                    self.i += 1;
                    return Ok(Json::Arr(a));
                }
                loop {
                    self.ws();
                    a.push(self.value(depth + 1)?);
                    self.ws();
                    match self.b.get(self.i) {
                        Some(b',') => self.i += 1,
                        Some(b']') => {
                            self.i += 1;
                            return Ok(Json::Arr(a));
                        }
                        _ => return self.err("',' or ']' is due"),
                    }
                }
            }
            Some(b'"') => Ok(Json::Str(self.string()?)),
            Some(b't') => self.lit("true", Json::Bool(true)),
            Some(b'f') => self.lit("false", Json::Bool(false)),
            Some(b'n') => self.lit("null", Json::Null),
            Some(_) => self.number(),
        }
    }
    fn number(&mut self) -> Result<Json, String> {
        let s = self.i;
        if self.b.get(self.i) == Some(&b'-') {
            self.i += 1;
        }
        match self.b.get(self.i) {
            Some(b'0') => self.i += 1,
            Some(b'1'..=b'9') => {
                while self.b.get(self.i).is_some_and(u8::is_ascii_digit) {
                    self.i += 1;
                }
            }
            _ => return self.err("not a JSON value"),
        }
        if self.b.get(self.i) == Some(&b'.') {
            self.i += 1;
            if !self.b.get(self.i).is_some_and(u8::is_ascii_digit) {
                return self.err("a digit is due after '.'");
            }
            while self.b.get(self.i).is_some_and(u8::is_ascii_digit) {
                self.i += 1;
            }
        }
        if matches!(self.b.get(self.i), Some(b'e' | b'E')) {
            self.i += 1;
            if matches!(self.b.get(self.i), Some(b'+' | b'-')) {
                self.i += 1;
            }
            if !self.b.get(self.i).is_some_and(u8::is_ascii_digit) {
                return self.err("a digit is due in the exponent");
            }
            while self.b.get(self.i).is_some_and(u8::is_ascii_digit) {
                self.i += 1;
            }
        }
        Ok(Json::Num(String::from_utf8(self.b[s..self.i].to_vec()).expect("ASCII")))
    }
    fn hex4(&mut self) -> Result<u32, String> {
        let h = self.b.get(self.i..self.i + 4).ok_or_else(|| format!("a \\u escape is cut short at byte {}", self.i))?;
        let s = std::str::from_utf8(h).map_err(|_| "a \\u escape is not hexadecimal".to_string())?;
        if !s.bytes().all(|c| c.is_ascii_hexdigit()) {
            return self.err("a \\u escape is not hexadecimal");
        }
        self.i += 4;
        Ok(u32::from_str_radix(s, 16).expect("hex"))
    }
    fn string(&mut self) -> Result<String, String> {
        self.i += 1; // the opening quote
        let mut out = String::new();
        loop {
            let Some(&c) = self.b.get(self.i) else { return self.err("a string is not closed") };
            match c {
                b'"' => {
                    self.i += 1;
                    return Ok(out);
                }
                b'\\' => {
                    self.i += 1;
                    let Some(&e) = self.b.get(self.i) else { return self.err("an escape is cut short") };
                    self.i += 1;
                    match e {
                        b'"' => out.push('"'),
                        b'\\' => out.push('\\'),
                        b'/' => out.push('/'),
                        b'b' => out.push('\u{8}'),
                        b'f' => out.push('\u{c}'),
                        b'n' => out.push('\n'),
                        b'r' => out.push('\r'),
                        b't' => out.push('\t'),
                        b'u' => {
                            let hi = self.hex4()?;
                            let cp = if (0xD800..0xDC00).contains(&hi) {
                                if self.b.get(self.i..self.i + 2) != Some(b"\\u") {
                                    return self.err("an escape leaves a surrogate unpaired");
                                }
                                self.i += 2;
                                let lo = self.hex4()?;
                                if !(0xDC00..0xE000).contains(&lo) {
                                    return self.err("an escape leaves a surrogate unpaired");
                                }
                                0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)
                            } else if (0xDC00..0xE000).contains(&hi) {
                                return self.err("an escape leaves a surrogate unpaired");
                            } else {
                                hi
                            };
                            out.push(char::from_u32(cp).expect("a scalar value"));
                        }
                        _ => return self.err("not a JSON escape"),
                    }
                }
                0x00..=0x1f => return self.err("a control character in a string"),
                _ => {
                    // Copy one UTF-8 character (the text is valid UTF-8).
                    let len = match c {
                        0x00..=0x7f => 1,
                        0xc0..=0xdf => 2,
                        0xe0..=0xef => 3,
                        _ => 4,
                    };
                    out.push_str(std::str::from_utf8(&self.b[self.i..self.i + len]).expect("UTF-8"));
                    self.i += len;
                }
            }
        }
    }
}

/// `s` as a JSON string, escaped as RFC 8785 §3.2.2.2 escapes it: `"` and `\` with a
/// backslash, U+0008, U+0009, U+000A, U+000C and U+000D by their short escapes, every other
/// control character as `\u00xx` with lowercase hexadecimal digits, and nothing else.
#[cfg_attr(not(any(feature = "prior_wire", feature = "scene_map")), allow(dead_code))]
pub fn quote(s: &str) -> String {
    let mut o = String::with_capacity(s.len() + 2);
    o.push('"');
    for c in s.chars() {
        match c {
            '"' => o.push_str("\\\""),
            '\\' => o.push_str("\\\\"),
            '\u{8}' => o.push_str("\\b"),
            '\t' => o.push_str("\\t"),
            '\n' => o.push_str("\\n"),
            '\u{c}' => o.push_str("\\f"),
            '\r' => o.push_str("\\r"),
            c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
            c => o.push(c),
        }
    }
    o.push('"');
    o
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn keeps_order_and_spelling_and_refuses_what_it_must() {
        let v = read(br#" {"b":[1,-2.50e+3,true,null],"a":"x\u00e9\ud83d\ude00"} "#).unwrap();
        let m = v.as_obj().unwrap();
        assert_eq!(m[0].0, "b");
        assert_eq!(m[1].1.as_str(), Some("x\u{e9}\u{1f600}"));
        assert_eq!(m[0].1.as_arr().unwrap()[1], Json::Num("-2.50e+3".into()));
        for bad in [&br#"{"a":1,"a":2}"#[..], b"[1,]", b"01", b"\"\\ud800\"", b"{} x", b"\"\x01\"", b"1.", b"-", b"\xff"] {
            assert!(read(bad).is_err(), "{:?}", String::from_utf8_lossy(bad));
        }
        assert_eq!(Json::Num("12".into()).as_u64(), Some(12));
        assert_eq!(Json::Num("012".into()).as_u64(), None);
        for (n, want) in [("-12", Some(-12)), ("-0", Some(0)), ("0", Some(0)), ("-9223372036854775808", Some(i64::MIN)),
                          ("9223372036854775807", Some(i64::MAX)), ("9223372036854775808", None), ("-012", None), ("1.0", None),
                          ("1e2", None), ("--1", None), ("-", None)] {
            assert_eq!(Json::Num(n.into()).as_i64(), want, "{n}");
        }
        assert_eq!(quote("a\"\u{1}\n"), "\"a\\\"\\u0001\\n\"");
        let deep = "[".repeat(MAX_DEPTH + 2) + &"]".repeat(MAX_DEPTH + 2);
        assert!(read(deep.as_bytes()).is_err());
    }
}
