//! A bundle as a USD layer in the text format of the USD Core Specification v1.0.1, and back
//! (`extensions/scene-carriage.md` §6 and §7).

use super::num::{f64_to_fx, fx_to_f64, usd_number};
use super::{
    add, check_bundle, hex, kinematic_state, media_from_json, media_json, parse_urn, position, refuse, unhex, world_bytes, Bundle,
    ImportParams, Imported, Refusal, ASSET_URN, PROFILE,
};

/// The largest time code the export writes: the binary64 integers are exact up to it.
pub const MAX_TIME_CODE: u64 = 1 << 53;

/// Whether `c` may appear in a USDA string unescaped or as one of its escapes (§6.3).
fn usd_char_ok(c: char) -> bool {
    let u = c as u32;
    matches!(u, 0x00..=0x1F | 0x20..=0x7E | 0x7F | 0xA0..=0xD7FF | 0xF900..=0xFDCF | 0xFDF0..=0xFFEF)
        || ((0x10000..=0xEFFFD).contains(&u) && (u & 0xFFFE) != 0xFFFE)
}

/// A USDA string (§6.3): `"`, `\` and the escapes `\n`, `\r`, `\t`, other control characters
/// and U+007F as `\x` and two uppercase hexadecimal digits.
pub fn usd_string(s: &str) -> Result<String, Refusal> {
    let mut o = String::from("\"");
    for c in s.chars() {
        if !usd_char_ok(c) {
            return refuse("not_presentable", format!("U+{:04X} has no USDA spelling", c as u32));
        }
        match c {
            '"' => o.push_str("\\\""),
            '\\' => o.push_str("\\\\"),
            '\n' => o.push_str("\\n"),
            '\r' => o.push_str("\\r"),
            '\t' => o.push_str("\\t"),
            c if (c as u32) < 0x20 || c == '\u{7f}' => o.push_str(&format!("\\x{:02X}", c as u32)),
            c => o.push(c),
        }
    }
    o.push('"');
    Ok(o)
}

fn tuple3(p: [i64; 3]) -> String {
    format!("({}, {}, {})", usd_number(fx_to_f64(p[0])), usd_number(fx_to_f64(p[1])), usd_number(fx_to_f64(p[2])))
}

struct Out {
    s: String,
}

impl Out {
    fn line(&mut self, depth: usize, t: &str) {
        self.s.push_str(&"    ".repeat(depth));
        self.s.push_str(t);
        self.s.push('\n');
    }
}

/// Write `bundle` as a USDA layer (§6), byte for byte.
pub fn export(bundle: &Bundle) -> Result<Vec<u8>, Refusal> {
    let (world, audio) = check_bundle(bundle)?;
    let mut o = Out { s: String::from("#usda 1.0\n(\n") };
    o.line(1, "customLayerData = {");
    if let Some(a) = &bundle.audio {
        o.line(2, &format!("string \"wai:audio\" = \"{}\"", hex(a)));
    }
    if !bundle.media.is_empty() {
        o.line(2, &format!("string \"wai:media\" = {}", usd_string(&media_json(&bundle.media))?));
    }
    o.line(2, &format!("string \"wai:profile\" = \"{PROFILE}\""));
    if let Some(w) = &bundle.world {
        o.line(2, &format!("string \"wai:world\" = \"{}\"", hex(w)));
    }
    o.line(1, "}");
    o.line(1, "defaultPrim = \"World\"");
    if let Some(a) = &audio {
        if a.duration > MAX_TIME_CODE || a.sample_rate > MAX_TIME_CODE {
            return refuse("not_presentable", "a duration past 2^53 time codes");
        }
        o.line(1, &format!("endTimeCode = {}", a.duration));
        o.line(1, "startTimeCode = 0");
        o.line(1, &format!("timeCodesPerSecond = {}", a.sample_rate));
    }
    o.s.push_str(")\n\ndef Xform \"World\"\n{\n");
    let mut first = true;
    let mut sep = |o: &mut Out| {
        if !first {
            o.s.push('\n');
        }
        first = false;
    };
    if let Some(w) = &world {
        for n in &w.nodes {
            sep(&mut o);
            o.line(1, &format!("def Xform \"node_{}\"", hex(&n.id)));
            o.line(1, "{");
            if let Some(p) = position(&w.sim, &n.state)? {
                o.line(2, &format!("double3 xformOp:translate = {}", tuple3(p)));
                o.line(2, "uniform token[] xformOpOrder = [\"xformOp:translate\"]");
            }
            o.line(1, "}");
        }
        if !w.assets.is_empty() {
            sep(&mut o);
            o.line(1, "def Scope \"Assets\"");
            o.line(1, "{");
            for (i, (h, role, name)) in w.assets.iter().enumerate() {
                if i > 0 {
                    o.s.push('\n');
                }
                o.line(2, &format!("def \"asset_{i}\" ("));
                o.line(3, &format!("prepend references = @{ASSET_URN}{}@", hex(h)));
                o.line(2, ")");
                o.line(2, "{");
                o.line(3, &format!("custom string wai:name = {}", usd_string(name)?));
                o.line(3, &format!("custom int wai:role = {role}"));
                o.line(2, "}");
            }
            o.line(1, "}");
        }
    }
    if let Some(a) = &audio
        && !a.objects.is_empty()
    {
        sep(&mut o);
        o.line(1, "def Scope \"Audio\"");
        o.line(1, "{");
        for (i, ob) in a.objects.iter().enumerate() {
            if i > 0 {
                o.s.push('\n');
            }
            for w in ob.keys.windows(2) {
                if w[1].sample <= w[0].sample {
                    return refuse("not_presentable", format!("object {i}: its keys are not in ascending sample order"));
                }
            }
            if ob.keys.iter().any(|k| k.sample > MAX_TIME_CODE) {
                return refuse("not_presentable", format!("object {i}: a key past 2^53 time codes"));
            }
            o.line(2, &format!("def Xform \"object_{i}\""));
            o.line(2, "{");
            o.line(3, &format!("custom string wai:source = \"{}\"", hex(&ob.source)));
            if let Some(k0) = ob.keys.first() {
                o.line(3, &format!("double3 xformOp:translate = {}", tuple3(k0.pos)));
                if ob.keys.len() >= 2 {
                    o.line(3, "double3 xformOp:translate.timeSamples = {");
                    for k in &ob.keys {
                        o.line(4, &format!("{}: {},", k.sample, tuple3(k.pos)));
                    }
                    o.line(3, "}");
                }
                o.line(3, "uniform token[] xformOpOrder = [\"xformOp:translate\"]");
            }
            o.line(2, "}");
        }
        o.line(1, "}");
    }
    if !bundle.media.is_empty() {
        sep(&mut o);
        o.line(1, "def Scope \"Media\"");
        o.line(1, "{");
        for (i, m) in bundle.media.iter().enumerate() {
            if i > 0 {
                o.s.push('\n');
            }
            o.line(2, &format!("def \"media_{i}\" ("));
            o.line(3, &format!("prepend references = @{ASSET_URN}{}@", hex(&m.asset)));
            o.line(2, ")");
            o.line(2, "{");
            o.line(3, &format!("custom string wai:capability = {}", usd_string(&m.capability)?));
            if let Some((_, h)) = m.size {
                o.line(3, &format!("custom int wai:height = {h}"));
            }
            o.line(3, &format!("custom string wai:name = {}", usd_string(&m.name)?));
            if let Some((w, _)) = m.size {
                o.line(3, &format!("custom int wai:width = {w}"));
            }
            o.line(2, "}");
        }
        o.line(1, "}");
    }
    o.s.push_str("}\n");
    Ok(o.s.into_bytes())
}

// ---------------------------------------------------------------------------------------
// Reading the subset of §7.1
// ---------------------------------------------------------------------------------------

#[derive(Clone, Debug, PartialEq)]
enum Tok {
    Punct(char),
    Ident(String),
    Str(String),
    Num(String),
    Asset(String),
    Path(String),
}

fn malformed(why: impl Into<String>) -> Refusal {
    Refusal::new("document_malformed", why)
}

fn not_read(why: impl Into<String>) -> Refusal {
    Refusal::new("usd_not_read", why)
}

fn lex(text: &str) -> Result<Vec<Tok>, Refusal> {
    let b: Vec<char> = text.chars().collect();
    let mut i = 0usize;
    let mut out = Vec::new();
    while i < b.len() {
        let c = b[i];
        if matches!(c, ' ' | '\t' | '\r' | '\n') {
            i += 1;
        } else if c == '#' || (c == '/' && b.get(i + 1) == Some(&'/')) {
            while i < b.len() && b[i] != '\n' && b[i] != '\r' {
                i += 1;
            }
        } else if c == '/' && b.get(i + 1) == Some(&'*') {
            i += 2;
            loop {
                if i + 1 >= b.len() {
                    return Err(malformed("a comment that does not end"));
                }
                if b[i] == '*' && b[i + 1] == '/' {
                    i += 2;
                    break;
                }
                i += 1;
            }
        } else if c == '-' || c.is_ascii_digit() || (c == '.' && b.get(i + 1).is_some_and(char::is_ascii_digit)) {
            let s0 = i;
            if c == '-' {
                i += 1;
            }
            if b[i..].starts_with(&['i', 'n', 'f']) {
                i += 3;
            } else {
                while i < b.len() && (b[i].is_ascii_digit() || b[i] == '.' || b[i] == 'e' || b[i] == 'E' || ((b[i] == '-' || b[i] == '+') && matches!(b[i - 1], 'e' | 'E'))) {
                    i += 1;
                }
            }
            out.push(Tok::Num(b[s0..i].iter().collect()));
        } else if "()[]{}=:,;.".contains(c) {
            out.push(Tok::Punct(c));
            i += 1;
        } else if c == '"' || c == '\'' {
            if b.get(i + 1) == Some(&c) && b.get(i + 2) == Some(&c) {
                return Err(not_read("a triple-quoted string"));
            }
            i += 1;
            let mut s = String::new();
            loop {
                let Some(&x) = b.get(i) else { return Err(malformed("a string that does not end")) };
                i += 1;
                if x == c {
                    break;
                }
                if x == '\n' || x == '\r' {
                    return Err(malformed("a line break in a string"));
                }
                if x != '\\' {
                    s.push(x);
                    continue;
                }
                let Some(&e) = b.get(i) else { return Err(malformed("an escape that does not end")) };
                i += 1;
                match e {
                    'n' => s.push('\n'),
                    'r' => s.push('\r'),
                    't' => s.push('\t'),
                    'a' => s.push('\u{7}'),
                    'b' => s.push('\u{8}'),
                    'f' => s.push('\u{c}'),
                    'v' => s.push('\u{b}'),
                    '\\' | '\'' | '"' => s.push(e),
                    'x' => {
                        let mut v = 0u32;
                        let mut n = 0;
                        while n < 2 && b.get(i).is_some_and(|d| d.is_ascii_digit() || ('A'..='F').contains(d)) {
                            v = v * 16 + b[i].to_digit(16).unwrap();
                            i += 1;
                            n += 1;
                        }
                        if n == 0 {
                            return Err(malformed("an empty \\x escape"));
                        }
                        if v >= 0x80 {
                            return Err(not_read("a \\x escape past U+007F"));
                        }
                        s.push(char::from_u32(v).unwrap());
                    }
                    _ => return Err(not_read(format!("the escape \\{e}"))),
                }
            }
            out.push(Tok::Str(s));
        } else if c == '@' {
            if b.get(i + 1) == Some(&'@') {
                return Err(not_read("a triple-@ asset path"));
            }
            i += 1;
            let mut s = String::new();
            loop {
                let Some(&x) = b.get(i) else { return Err(malformed("an asset path that does not end")) };
                i += 1;
                if x == '@' {
                    break;
                }
                if x == '\n' || x == '\r' {
                    return Err(malformed("a line break in an asset path"));
                }
                s.push(x);
            }
            out.push(Tok::Asset(s));
        } else if c == '<' {
            let mut s = String::new();
            i += 1;
            loop {
                let Some(&x) = b.get(i) else { return Err(malformed("a path that does not end")) };
                i += 1;
                if x == '>' {
                    break;
                }
                s.push(x);
            }
            out.push(Tok::Path(s));
        } else if c.is_ascii_alphabetic() || c == '_' {
            let s0 = i;
            while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == '_' || (b[i] == ':' && b.get(i + 1).is_some_and(|d| d.is_ascii_alphabetic() || *d == '_'))) {
                i += 1;
            }
            out.push(Tok::Ident(b[s0..i].iter().collect()));
        } else {
            return Err(not_read(format!("the character {c:?}")));
        }
    }
    Ok(out)
}

/// A value of the subset.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Val {
    Num(String),
    Str(String),
    Ident(String),
    Asset(String),
    Path(String),
    Tuple(Vec<Val>),
    List(Vec<Val>),
    Dict(Vec<(String, String, Val)>),
    Samples(Vec<(String, Val)>),
}

/// A property of a prim: its type, name and default value, and its time samples.
#[derive(Clone, Debug, Default)]
pub(crate) struct Prop {
    pub ty: String,
    pub default: Option<Val>,
    pub samples: Option<Val>,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Prim {
    pub ty: String,
    pub name: String,
    pub references: Vec<String>,
    pub props: Vec<(String, Prop)>,
    pub children: Vec<Prim>,
}

#[derive(Debug, Default)]
pub(crate) struct Layer {
    pub custom: Vec<(String, Val)>,
    pub default_prim: Option<String>,
    pub prims: Vec<Prim>,
}

struct P {
    t: Vec<Tok>,
    i: usize,
}

impl P {
    fn peek(&self) -> Option<&Tok> {
        self.t.get(self.i)
    }
    fn next(&mut self) -> Result<Tok, Refusal> {
        let t = self.t.get(self.i).cloned().ok_or_else(|| malformed("the layer ends early"))?;
        self.i += 1;
        Ok(t)
    }
    fn punct(&mut self, c: char) -> Result<(), Refusal> {
        match self.next()? {
            Tok::Punct(x) if x == c => Ok(()),
            t => Err(malformed(format!("{c:?} is due, not {t:?}"))),
        }
    }
    fn is(&self, c: char) -> bool {
        self.peek() == Some(&Tok::Punct(c))
    }
    fn ident(&mut self) -> Result<String, Refusal> {
        match self.next()? {
            Tok::Ident(s) => Ok(s),
            t => Err(malformed(format!("a name is due, not {t:?}"))),
        }
    }
    fn skip_sep(&mut self) {
        while self.is(';') {
            self.i += 1;
        }
    }
    fn value(&mut self) -> Result<Val, Refusal> {
        match self.next()? {
            Tok::Num(s) => Ok(Val::Num(s)),
            Tok::Str(s) => Ok(Val::Str(s)),
            Tok::Ident(s) => Ok(Val::Ident(s)),
            Tok::Asset(s) => Ok(Val::Asset(s)),
            Tok::Path(s) => Ok(Val::Path(s)),
            Tok::Punct('(') => {
                let mut v = Vec::new();
                while !self.is(')') {
                    v.push(self.value()?);
                    if !self.is(')') {
                        self.punct(',')?;
                    }
                }
                self.i += 1;
                Ok(Val::Tuple(v))
            }
            Tok::Punct('[') => {
                let mut v = Vec::new();
                while !self.is(']') {
                    v.push(self.value()?);
                    if !self.is(']') {
                        self.punct(',')?;
                    }
                }
                self.i += 1;
                Ok(Val::List(v))
            }
            Tok::Punct('{') => self.dict_body(),
            t => Err(malformed(format!("a value is due, not {t:?}"))),
        }
    }
    /// A dictionary's items after its `{`: `type key = value` and `dictionary key = {…}`.
    fn dict_body(&mut self) -> Result<Val, Refusal> {
        let mut items = Vec::new();
        loop {
            self.skip_sep();
            if self.is('}') {
                self.i += 1;
                return Ok(Val::Dict(items));
            }
            let ty = self.ident()?;
            let mut ty2 = ty.clone();
            if self.is('[') {
                self.i += 1;
                self.punct(']')?;
                ty2.push_str("[]");
            }
            let key = match self.next()? {
                Tok::Str(s) | Tok::Ident(s) => s,
                t => return Err(malformed(format!("a dictionary key is due, not {t:?}"))),
            };
            self.punct('=')?;
            let v = if ty == "dictionary" {
                self.punct('{')?;
                self.dict_body()?
            } else {
                self.value()?
            };
            items.push((ty2, key, v));
        }
    }
    fn samples(&mut self) -> Result<Val, Refusal> {
        self.punct('{')?;
        let mut v = Vec::new();
        while !self.is('}') {
            let Tok::Num(t) = self.next()? else { return Err(malformed("a time code is due")) };
            self.punct(':')?;
            v.push((t, self.value()?));
            if !self.is('}') {
                self.punct(',')?;
            }
        }
        self.i += 1;
        Ok(Val::Samples(v))
    }
    fn prim(&mut self) -> Result<Prim, Refusal> {
        match self.ident()?.as_str() {
            "def" => {}
            s @ ("over" | "class") => return Err(not_read(format!("a prim specified by {s}"))),
            s => return Err(malformed(format!("a prim is due, not {s}"))),
        }
        let mut p = Prim::default();
        if let Some(Tok::Ident(t)) = self.peek() {
            p.ty = t.clone();
            self.i += 1;
        }
        let Tok::Str(name) = self.next()? else { return Err(malformed("a prim's name is due")) };
        p.name = name;
        if self.is('(') {
            self.i += 1;
            loop {
                self.skip_sep();
                if self.is(')') {
                    self.i += 1;
                    break;
                }
                if let Some(Tok::Str(_)) = self.peek() {
                    self.i += 1; // a comment
                    continue;
                }
                let mut key = self.ident()?;
                let mut op = None;
                if matches!(key.as_str(), "prepend" | "append" | "delete" | "add" | "reorder") {
                    op = Some(key);
                    key = self.ident()?;
                }
                match key.as_str() {
                    "references" => {
                        if op.as_deref().is_some_and(|o| o != "prepend" && o != "append") {
                            return Err(not_read(format!("{} references", op.unwrap())));
                        }
                        self.punct('=')?;
                        let v = self.value()?;
                        let items = match v {
                            Val::List(v) => v,
                            Val::Ident(s) if s == "None" => Vec::new(),
                            v => vec![v],
                        };
                        for it in items {
                            match it {
                                Val::Asset(a) => {
                                    if matches!(self.peek(), Some(Tok::Path(_))) || self.is('(') {
                                        return Err(not_read("a reference with a prim path or parameters"));
                                    }
                                    p.references.push(a)
                                }
                                _ => return Err(not_read("a reference that is not an asset path")),
                            }
                        }
                    }
                    "payload" | "inherits" | "specializes" | "variants" | "variantSets" | "relocates" => {
                        return Err(not_read(format!("the prim metadata {key}")));
                    }
                    _ => {
                        if op.is_some() {
                            return Err(not_read(format!("a list operation on {key}")));
                        }
                        self.punct('=')?;
                        self.value()?;
                    }
                }
            }
        }
        self.punct('{')?;
        loop {
            self.skip_sep();
            if self.is('}') {
                self.i += 1;
                break;
            }
            match self.peek() {
                Some(Tok::Ident(k)) if matches!(k.as_str(), "def" | "over" | "class") => {
                    let c = self.prim()?;
                    if p.children.iter().any(|x| x.name == c.name) {
                        return Err(malformed(format!("the prim {:?} twice", c.name)));
                    }
                    p.children.push(c);
                }
                Some(Tok::Ident(k)) if matches!(k.as_str(), "rel" | "variantSet" | "reorder" | "prepend" | "append" | "delete" | "add") => {
                    return Err(not_read(format!("the statement {k}")));
                }
                _ => {
                    let (name, prop, special) = self.property()?;
                    let entry = p.props.iter().position(|(n, _)| *n == name);
                    let e = match entry {
                        Some(i) => &mut p.props[i].1,
                        None => {
                            p.props.push((name.clone(), Prop::default()));
                            &mut p.props.last_mut().unwrap().1
                        }
                    };
                    if !e.ty.is_empty() && e.ty != prop.ty {
                        return Err(malformed(format!("the property {name} with two types")));
                    }
                    e.ty = prop.ty;
                    if special {
                        if e.samples.is_some() {
                            return Err(malformed(format!("{name}.timeSamples twice")));
                        }
                        e.samples = prop.samples;
                    } else {
                        if e.default.is_some() {
                            return Err(malformed(format!("{name} twice")));
                        }
                        e.default = prop.default;
                    }
                }
            }
        }
        Ok(p)
    }
    /// An attribute: `[custom] [uniform|varying] type[[]] name` then `= value`, `.timeSamples
    /// = {…}`, or nothing, then optional metadata.
    fn property(&mut self) -> Result<(String, Prop, bool), Refusal> {
        let mut t = self.ident()?;
        if t == "custom" {
            t = self.ident()?;
        }
        if t == "uniform" || t == "varying" {
            t = self.ident()?;
        }
        let mut ty = t;
        if self.is('[') {
            self.i += 1;
            self.punct(']')?;
            ty.push_str("[]");
        }
        let name = self.ident()?;
        let mut prop = Prop { ty, ..Prop::default() };
        let mut special = false;
        if self.is('.') {
            self.i += 1;
            let s = self.ident()?;
            if s != "timeSamples" {
                return Err(not_read(format!("{name}.{s}")));
            }
            self.punct('=')?;
            prop.samples = Some(self.samples()?);
            special = true;
        } else if self.is('=') {
            self.i += 1;
            prop.default = Some(self.value()?);
        }
        if self.is('(') {
            // attribute metadata: read and ignored
            self.i += 1;
            loop {
                self.skip_sep();
                if self.is(')') {
                    self.i += 1;
                    break;
                }
                if let Some(Tok::Str(_)) = self.peek() {
                    self.i += 1;
                    continue;
                }
                self.ident()?;
                self.punct('=')?;
                self.value()?;
            }
        }
        Ok((name, prop, special))
    }
}

pub(crate) fn parse(bytes: &[u8]) -> Result<Layer, Refusal> {
    let text = std::str::from_utf8(bytes).map_err(|_| malformed("not UTF-8"))?;
    let rest = text.strip_prefix("#usda").ok_or_else(|| malformed("no #usda header"))?;
    let line_end = rest.find(['\n', '\r']).unwrap_or(rest.len());
    if rest[..line_end].trim_matches([' ', '\t']) != "1.0" {
        return Err(not_read(format!("the layer version {:?}", rest[..line_end].trim())));
    }
    let mut p = P { t: lex(&rest[line_end..])?, i: 0 };
    let mut layer = Layer::default();
    if p.is('(') {
        p.i += 1;
        loop {
            p.skip_sep();
            if p.is(')') {
                p.i += 1;
                break;
            }
            if let Some(Tok::Str(_)) = p.peek() {
                p.i += 1;
                continue;
            }
            let key = p.ident()?;
            if matches!(key.as_str(), "subLayers" | "relocates" | "prepend" | "append" | "delete") {
                return Err(not_read(format!("the layer metadata {key}")));
            }
            p.punct('=')?;
            let v = p.value()?;
            match key.as_str() {
                "customLayerData" => {
                    let Val::Dict(items) = v else { return Err(malformed("customLayerData is not a dictionary")) };
                    for (ty, k, v) in items {
                        if ty == "string" {
                            layer.custom.push((k, v));
                        }
                    }
                }
                "defaultPrim" => {
                    let Val::Str(s) = v else { return Err(malformed("defaultPrim is not a string")) };
                    layer.default_prim = Some(s);
                }
                _ => {}
            }
        }
    }
    while p.peek().is_some() {
        let c = p.prim()?;
        if layer.prims.iter().any(|x| x.name == c.name) {
            return Err(malformed(format!("the prim {:?} twice", c.name)));
        }
        layer.prims.push(c);
    }
    Ok(layer)
}

/// Read `bytes` as a USDA layer (§7): one WAI wrote, back to its bundle; any other, to a new
/// bundle under §7.2 with `params`.
pub fn import(bytes: &[u8], params: &ImportParams) -> Result<Imported, Refusal> {
    let layer = parse(bytes)?;
    let wai: Vec<&(String, Val)> = layer.custom.iter().filter(|(k, _)| k.starts_with("wai:")).collect();
    if !wai.is_empty() {
        let cm = |w: &str| Refusal::new("carried_malformed", w.to_string());
        let mut b = Bundle::default();
        let mut profile = false;
        for (k, v) in wai {
            let Val::Str(s) = v else { return Err(cm("a carried value that is not a string")) };
            match k.as_str() {
                "wai:profile" if s == PROFILE => profile = true,
                "wai:world" => b.world = Some(unhex(s).ok_or_else(|| cm("wai:world is not hexadecimal"))?),
                "wai:audio" => b.audio = Some(unhex(s).ok_or_else(|| cm("wai:audio is not hexadecimal"))?),
                "wai:media" => {
                    let j = crate::strict_json::read(s.as_bytes()).map_err(|_| cm("wai:media is not JSON"))?;
                    b.media = media_from_json(&j).ok_or_else(|| cm("wai:media is not a media list"))?;
                }
                _ => return Err(cm(&format!("the key {k:?}"))),
            }
        }
        if !profile {
            return Err(cm("no wai:profile wai-scene/1"));
        }
        let again = export(&b).map_err(|r| Refusal::new("carried_malformed", r.to_string()))?;
        if again != bytes {
            return refuse("presentation_mismatch", "the layer is not the export of the bundle it carries");
        }
        return Ok(Imported { bundle: b, dropped: Vec::new() });
    }
    foreign(&layer, params)
}

fn translate(p: &Prim) -> Result<[i64; 3], Refusal> {
    let mut t = [0i64; 3];
    for (name, prop) in &p.props {
        if name == "xformOpOrder" {
            let ok = matches!(&prop.default, Some(Val::List(v)) if v.len() == 1 && v[0] == Val::Str("xformOp:translate".into()));
            if !ok || prop.samples.is_some() {
                return refuse("transform_not_carried", format!("{}: xformOpOrder", p.name));
            }
        } else if name == "xformOp:translate" {
            if prop.samples.is_some() {
                return refuse("transform_not_carried", format!("{}: a translation over time", p.name));
            }
            let Some(Val::Tuple(v)) = &prop.default else { return refuse("transform_not_carried", format!("{}: no default translation", p.name)) };
            if v.len() != 3 {
                return Err(malformed(format!("{}: a translation of {} values", p.name, v.len())));
            }
            for (i, x) in v.iter().enumerate() {
                let Val::Num(s) = x else { return Err(malformed(format!("{}: a translation that is not numbers", p.name))) };
                let f: f64 = s.parse().map_err(|_| malformed(format!("{}: {s}", p.name)))?;
                t[i] = f64_to_fx(f).ok_or_else(|| Refusal::new("position_not_exact", format!("{}: {s}", p.name)))?;
            }
        } else if name.starts_with("xformOp:") {
            return refuse("transform_not_carried", format!("{}: {name}", p.name));
        }
    }
    Ok(t)
}

fn foreign(layer: &Layer, params: &ImportParams) -> Result<Imported, Refusal> {
    let root = match &layer.default_prim {
        Some(n) => layer.prims.iter().find(|p| p.name == *n).ok_or_else(|| malformed(format!("defaultPrim {n:?} is not a root prim")))?,
        None => layer.prims.first().ok_or_else(|| malformed("no prim"))?,
    };
    let mut nodes: Vec<[i64; 3]> = Vec::new();
    let mut assets: Vec<(Vec<u8>, u8, String)> = Vec::new();
    let mut dropped = Vec::new();
    // Depth first, written order, the root included.
    let mut stack: Vec<(&Prim, [i64; 3], String)> = vec![(root, [0; 3], format!("/{}", root.name))];
    while let Some((p, base, path)) = stack.pop() {
        let at = add(base, translate(p)?).ok_or_else(|| Refusal::new("position_not_exact", format!("{path}: past the Q32.32 range")))?;
        if !p.references.is_empty() {
            for r in &p.references {
                let h = parse_urn(r).ok_or_else(|| Refusal::new("asset_not_wai", format!("{path}: @{r}@")))?;
                assets.push((h.to_vec(), 0, p.name.clone()));
            }
        } else if p.ty == "Xform" && p.props.iter().any(|(n, _)| n == "xformOp:translate") {
            nodes.push(at);
        } else if !matches!(p.ty.as_str(), "" | "Xform" | "Scope") {
            dropped.push(format!("{path}: {}", p.ty));
        }
        for c in p.children.iter().rev() {
            stack.push((c, at, format!("{path}/{}", c.name)));
        }
    }
    let mut bundle = Bundle::default();
    if !nodes.is_empty() || !assets.is_empty() {
        let c = params.world_contract.as_ref().ok_or_else(|| Refusal::new("contract_absent", "world nodes or assets and no world contract"))?;
        let j = crate::strict_json::read(c).map_err(|_| Refusal::new("contract_absent", "the world contract is not JSON"))?;
        if j.get("sim").and_then(|v| v.as_str()) != Some("wai.world.sim.kinematic0") {
            return refuse("sim_not_importable", "an imported world's sim is wai.world.sim.kinematic0");
        }
        let mut ns: Vec<([u8; 16], Vec<u8>)> = nodes.iter().enumerate().map(|(i, p)| (crate::world::mint_node_id(&params.actor, i as u64 + 1), kinematic_state(*p))).collect();
        bundle.world = Some(world_bytes(c, &mut ns, &assets));
    }
    Ok(Imported { bundle, dropped })
}

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

    #[test]
    fn strings_escape_as_stated() {
        assert_eq!(usd_string("a\"b\\c\n\u{1}\u{7f}é").unwrap(), "\"a\\\"b\\\\c\\n\\x01\\x7Fé\"");
        assert_eq!(usd_string("\u{e000}").unwrap_err().code, "not_presentable");
        assert_eq!(usd_string("\u{fffe}").unwrap_err().code, "not_presentable");
    }

    #[test]
    fn the_subset_reads_and_refuses_as_stated() {
        let l = parse(b"#usda 1.0\n(\n    defaultPrim = \"W\" // c\n)\ndef Xform \"W\" (\n    kind = \"group\"\n)\n{\n    def Xform \"a\" { double3 xformOp:translate = (1, -2.5, 0.25) ; uniform token[] xformOpOrder = [\"xformOp:translate\"] }\n    /* x */ def \"r\" ( prepend references = @urn:wai:asset:00@ ) { }\n}\n").unwrap();
        assert_eq!(l.prims[0].children.len(), 2);
        let code = |s: &str| parse(s.as_bytes()).err().map(|r| r.code);
        assert_eq!(code("#usda 1.1\n"), Some("usd_not_read"));
        assert_eq!(code("#usda 1.0\nover \"x\" {}\n"), Some("usd_not_read"));
        assert_eq!(code("#usda 1.0\ndef \"x\" { rel r = </a> }\n"), Some("usd_not_read"));
        assert_eq!(code("#usda 1.0\ndef \"x\" { int a = 1\n int a = 2 }\n"), Some("document_malformed"));
        assert_eq!(code("#usda 1.0\ndef \"x\" {\n"), Some("document_malformed"));
    }
}
