//! Schema-validated generators for skeleton, attention, and genepool catalogs.

use super::{InputSpec, generated_rust, normalize_text, verify_inputs};
use roxmltree::{Document, Node};
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

#[derive(Clone, Debug, PartialEq)]
struct JointBaseRecord {
    name: String,
    pos: Vec<f32>,
    rot: Vec<f32>,
    scale: Vec<f32>,
    group: String,
    support: String,
    end: Vec<f32>,
    reposition: bool,
}

#[derive(Clone, Debug, PartialEq)]
struct CollisionRecord {
    base: JointBaseRecord,
}

#[derive(Clone, Debug, PartialEq)]
struct JointRecord {
    base: JointBaseRecord,
    collision_volumes: Vec<CollisionRecord>,
    bones: Vec<JointRecord>,
    pivot: Vec<f32>,
    aliases: String,
    connected: bool,
}

#[derive(Clone, Debug, PartialEq)]
struct SkeletonRecord {
    version: f32,
    num_bones: String,
    num_collision_volumes: String,
    root: JointRecord,
}

#[derive(Clone, Copy, Debug, PartialEq)]
struct AttentionRecord {
    priority: f32,
    timeout: f32,
}

#[derive(Clone, Debug, PartialEq)]
struct AttentionSetRecord {
    name: String,
    entries: [AttentionRecord; 11],
}

#[derive(Clone, Debug, PartialEq)]
struct ArchetypeParamRecord {
    id: i32,
    name: String,
    value: f32,
}

#[derive(Clone, Debug, PartialEq)]
struct ArchetypeRecord {
    name: String,
    params: Vec<ArchetypeParamRecord>,
}

const ATTENTION_NAMES: [&str; 9] = [
    "idle",
    "auto_listen",
    "freelook",
    "respond",
    "hover",
    "conversation",
    "select",
    "focus",
    "mouselook",
];

fn location(path: &str, node: Node<'_, '_>, code: &str, message: &str) -> String {
    let position = node.document().text_pos_at(node.range().start);
    format!(
        "{path}:{}:{}: error[{code}]: {message}",
        position.row, position.col
    )
}

fn required_attribute(
    path: &str,
    node: Node<'_, '_>,
    name: &str,
    code: &str,
) -> Result<String, String> {
    node.attribute(name)
        .filter(|value| !value.is_empty())
        .map(str::to_owned)
        .ok_or_else(|| {
            location(
                path,
                node,
                code,
                &format!("<{}> requires attribute {name:?}", node.tag_name().name()),
            )
        })
}

fn parse_f32(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result<f32, String> {
    let value = required_attribute(path, node, name, code)?;
    let parsed = value.parse::<f32>().map_err(|_| {
        location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be a finite float, found {value:?}"),
        )
    })?;
    if parsed.is_finite() {
        Ok(parsed)
    } else {
        Err(location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be a finite float, found {value:?}"),
        ))
    }
}

fn parse_i32(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result<i32, String> {
    let value = required_attribute(path, node, name, code)?;
    value.parse::<i32>().map_err(|_| {
        location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be a signed 32-bit integer, found {value:?}"),
        )
    })
}

fn parse_usize(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result<usize, String> {
    let value = required_attribute(path, node, name, code)?;
    value.parse::<usize>().map_err(|_| {
        location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be a non-negative integer, found {value:?}"),
        )
    })
}

fn vector_attribute(
    path: &str,
    node: Node<'_, '_>,
    name: &str,
    code: &str,
) -> Result<Vec<f32>, String> {
    let Some(value) = node.attribute(name) else {
        return Ok(Vec::new());
    };
    let values = value
        .split_ascii_whitespace()
        .map(str::parse::<f32>)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|_| {
            location(
                path,
                node,
                code,
                &format!("attribute {name:?} must contain three finite floats"),
            )
        })?;
    if values.len() != 3 || values.iter().any(|value| !value.is_finite()) {
        return Err(location(
            path,
            node,
            code,
            &format!("attribute {name:?} must contain three finite floats"),
        ));
    }
    Ok(values)
}

fn boolean_attribute(
    path: &str,
    node: Node<'_, '_>,
    name: &str,
    code: &str,
) -> Result<bool, String> {
    match node.attribute(name) {
        None => Ok(false),
        Some(value) if value.eq_ignore_ascii_case("true") => Ok(true),
        Some(value) if value.eq_ignore_ascii_case("false") => Ok(false),
        Some(value) => Err(location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be true or false, found {value:?}"),
        )),
    }
}

fn parse_joint_base(
    path: &str,
    node: Node<'_, '_>,
    name_code: &str,
) -> Result<JointBaseRecord, String> {
    let support = node.attribute("support").unwrap_or_default().to_owned();
    if !support.is_empty()
        && !support.eq_ignore_ascii_case("base")
        && !support.eq_ignore_ascii_case("extended")
    {
        return Err(location(
            path,
            node,
            "SK018",
            &format!("unknown skeleton support category {support:?}"),
        ));
    }
    Ok(JointBaseRecord {
        name: required_attribute(path, node, "name", name_code)?,
        pos: vector_attribute(path, node, "pos", "SK011")?,
        rot: vector_attribute(path, node, "rot", "SK012")?,
        scale: vector_attribute(path, node, "scale", "SK013")?,
        group: node.attribute("group").unwrap_or_default().to_owned(),
        support,
        end: vector_attribute(path, node, "end", "SK014")?,
        reposition: boolean_attribute(path, node, "reposition", "SK015")?,
    })
}

fn parse_joint(
    path: &str,
    node: Node<'_, '_>,
    identifiers: &mut BTreeSet<String>,
    collision_names: &mut BTreeSet<String>,
) -> Result<JointRecord, String> {
    let base = parse_joint_base(path, node, "SK010")?;
    if !identifiers.insert(base.name.clone()) {
        return Err(location(
            path,
            node,
            "SK016",
            &format!("duplicate skeleton joint identifier {:?}", base.name),
        ));
    }
    let aliases = node.attribute("aliases").unwrap_or_default().to_owned();
    for alias in aliases.split_ascii_whitespace() {
        if !identifiers.insert(alias.to_owned()) {
            return Err(location(
                path,
                node,
                "SK016",
                &format!("duplicate skeleton joint identifier {alias:?}"),
            ));
        }
    }
    let mut collision_volumes = Vec::new();
    let mut bones = Vec::new();
    for child in node.children().filter(Node::is_element) {
        if child.has_tag_name("collision_volume") {
            if child.children().any(|node| node.is_element()) {
                return Err(location(
                    path,
                    child,
                    "SK019",
                    "<collision_volume> may not contain child elements",
                ));
            }
            let collision = CollisionRecord {
                base: parse_joint_base(path, child, "SK017")?,
            };
            if !collision_names.insert(collision.base.name.clone()) {
                return Err(location(
                    path,
                    child,
                    "SK020",
                    &format!("duplicate collision-volume name {:?}", collision.base.name),
                ));
            }
            collision_volumes.push(collision);
        } else if child.has_tag_name("bone") {
            bones.push(parse_joint(path, child, identifiers, collision_names)?);
        } else {
            return Err(location(
                path,
                child,
                "SK021",
                "<bone> may contain only <bone> and <collision_volume> elements",
            ));
        }
    }
    Ok(JointRecord {
        base,
        collision_volumes,
        bones,
        pivot: vector_attribute(path, node, "pivot", "SK022")?,
        aliases,
        connected: boolean_attribute(path, node, "connected", "SK023")?,
    })
}

fn joint_counts(root: &JointRecord) -> (usize, usize) {
    let mut bones = 1;
    let mut collision_volumes = root.collision_volumes.len();
    for child in &root.bones {
        let (child_bones, child_collision_volumes) = joint_counts(child);
        bones += child_bones;
        collision_volumes += child_collision_volumes;
    }
    (bones, collision_volumes)
}

fn parse_skeleton(path: &str, text: &str) -> Result<SkeletonRecord, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("linden_skeleton") {
        return Err(location(
            path,
            root,
            "SK001",
            "root element must be <linden_skeleton>",
        ));
    }
    let version = parse_f32(path, root, "version", "SK002")?;
    let expected_bones = parse_usize(path, root, "num_bones", "SK003")?;
    let expected_collision_volumes = parse_usize(path, root, "num_collision_volumes", "SK004")?;
    let children = root.children().filter(Node::is_element).collect::<Vec<_>>();
    if children.len() != 1 || !children[0].has_tag_name("bone") {
        return Err(location(
            path,
            root,
            "SK005",
            "skeleton requires exactly one root <bone>",
        ));
    }
    let mut identifiers = BTreeSet::new();
    let mut collision_names = BTreeSet::new();
    let joint = parse_joint(path, children[0], &mut identifiers, &mut collision_names)?;
    let (actual_bones, actual_collision_volumes) = joint_counts(&joint);
    if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes {
        return Err(location(
            path,
            root,
            "SK006",
            &format!(
                "declared skeleton counts are {expected_bones} bones/{expected_collision_volumes} collision volumes but parsed {actual_bones}/{actual_collision_volumes}"
            ),
        ));
    }
    Ok(SkeletonRecord {
        version,
        num_bones: expected_bones.to_string(),
        num_collision_volumes: expected_collision_volumes.to_string(),
        root: joint,
    })
}

fn attention_index(name: &str) -> Option<usize> {
    ATTENTION_NAMES
        .iter()
        .position(|candidate| candidate.eq_ignore_ascii_case(name))
        .map(|index| index + 1)
}

fn parse_attentions(path: &str, text: &str) -> Result<Vec<AttentionSetRecord>, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("linden_attentions") {
        return Err(location(
            path,
            root,
            "AT001",
            "root element must be <linden_attentions>",
        ));
    }
    let _version = parse_f32(path, root, "version", "AT002")?;
    let genders = root.children().filter(Node::is_element).collect::<Vec<_>>();
    if genders.len() != 2 {
        return Err(location(
            path,
            root,
            "AT003",
            "attention catalog requires Masculine and Feminine gender sets",
        ));
    }
    let mut sets = Vec::new();
    for (gender_index, gender) in genders.into_iter().enumerate() {
        if !gender.has_tag_name("gender") {
            return Err(location(
                path,
                gender,
                "AT004",
                "<linden_attentions> may contain only <gender> elements",
            ));
        }
        let name = required_attribute(path, gender, "name", "AT005")?;
        let expected_name = if gender_index == 0 {
            "Masculine"
        } else {
            "Feminine"
        };
        if name != expected_name {
            return Err(location(
                path,
                gender,
                "AT006",
                &format!("gender set {gender_index} must be named {expected_name:?}"),
            ));
        }
        let mut entries = [AttentionRecord {
            priority: 0.0,
            timeout: 0.0,
        }; 11];
        let mut seen = BTreeSet::new();
        for param in gender.children().filter(Node::is_element) {
            if !param.has_tag_name("param") {
                return Err(location(
                    path,
                    param,
                    "AT007",
                    "<gender> may contain only <param> elements",
                ));
            }
            let attention = required_attribute(path, param, "attention", "AT008")?;
            let Some(index) = attention_index(&attention) else {
                return Err(location(
                    path,
                    param,
                    "AT009",
                    &format!("unknown attention name {attention:?}"),
                ));
            };
            if !seen.insert(index) {
                return Err(location(
                    path,
                    param,
                    "AT010",
                    &format!("duplicate attention name {attention:?}"),
                ));
            }
            entries[index] = AttentionRecord {
                priority: parse_f32(path, param, "priority", "AT011")?,
                timeout: parse_f32(path, param, "timeout", "AT012")?,
            };
        }
        if seen.len() != ATTENTION_NAMES.len() {
            return Err(location(
                path,
                gender,
                "AT013",
                "gender set must define each of the nine attention parameters exactly once",
            ));
        }
        sets.push(AttentionSetRecord { name, entries });
    }
    Ok(sets)
}

fn parse_genepool(path: &str, text: &str) -> Result<Vec<ArchetypeRecord>, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("linden_genepool") {
        return Err(location(
            path,
            root,
            "GP001",
            "root element must be <linden_genepool>",
        ));
    }
    let _version = parse_f32(path, root, "version", "GP002")?;
    let mut names = BTreeSet::new();
    let mut archetypes = Vec::new();
    for archetype in root.children().filter(Node::is_element) {
        if !archetype.has_tag_name("archetype") {
            return Err(location(
                path,
                archetype,
                "GP003",
                "<linden_genepool> may contain only <archetype> elements",
            ));
        }
        let name = required_attribute(path, archetype, "name", "GP004")?
            .trim()
            .to_owned();
        if name.is_empty() || !names.insert(name.clone()) {
            return Err(location(
                path,
                archetype,
                "GP005",
                &format!("empty or duplicate archetype name {name:?}"),
            ));
        }
        let mut ids = BTreeSet::new();
        let mut texture_slots = BTreeSet::new();
        let mut params = Vec::new();
        for child in archetype.children().filter(Node::is_element) {
            if child.has_tag_name("texture") {
                let slot = parse_i32(path, child, "te", "GP013")?;
                let uuid = required_attribute(path, child, "uuid", "GP014")?;
                if !texture_slots.insert(slot) {
                    return Err(location(
                        path,
                        child,
                        "GP015",
                        &format!("duplicate texture-entry slot {slot} in archetype {name:?}"),
                    ));
                }
                if !valid_uuid(&uuid) {
                    return Err(location(
                        path,
                        child,
                        "GP016",
                        &format!("invalid archetype texture UUID {uuid:?}"),
                    ));
                }
                continue;
            }
            if !child.has_tag_name("param") {
                return Err(location(
                    path,
                    child,
                    "GP006",
                    "<archetype> may contain only <param> and <texture> elements",
                ));
            }
            let id = parse_i32(path, child, "id", "GP007")?;
            if !ids.insert(id) {
                return Err(location(
                    path,
                    child,
                    "GP008",
                    &format!("duplicate parameter ID {id} in archetype {name:?}"),
                ));
            }
            params.push(ArchetypeParamRecord {
                id,
                name: required_attribute(path, child, "name", "GP009")?,
                value: parse_f32(path, child, "value", "GP010")?,
            });
        }
        if params.is_empty() {
            return Err(location(
                path,
                archetype,
                "GP011",
                "archetype must contain at least one parameter",
            ));
        }
        archetypes.push(ArchetypeRecord { name, params });
    }
    if archetypes.is_empty() {
        return Err(location(
            path,
            root,
            "GP012",
            "genepool must contain at least one archetype",
        ));
    }
    Ok(archetypes)
}

fn valid_uuid(value: &str) -> bool {
    value.len() == 36
        && value.bytes().enumerate().all(|(index, byte)| match index {
            8 | 13 | 18 | 23 => byte == b'-',
            _ => byte.is_ascii_hexdigit(),
        })
}

fn rust_f32(value: f32) -> String {
    format!("{value:?}_f32")
}

fn vector_expression(values: &[f32]) -> String {
    format!(
        "vec![{}]",
        values
            .iter()
            .map(|value| rust_f32(*value))
            .collect::<Vec<_>>()
            .join(",")
    )
}

fn base_expression(base: &JointBaseRecord) -> String {
    format!(
        "crate::skeleton::JointBase::from_parts({:?}.to_owned(), {}, {}, {}, {:?}.to_owned(), {:?}.to_owned(), {}, {})",
        base.name,
        vector_expression(&base.pos),
        vector_expression(&base.rot),
        vector_expression(&base.scale),
        base.group,
        base.support,
        vector_expression(&base.end),
        base.reposition
    )
}

fn joint_expression(joint: &JointRecord) -> String {
    let collision_volumes = joint
        .collision_volumes
        .iter()
        .map(|volume| {
            format!(
                "crate::skeleton::CollisionVolume::from_base({})",
                base_expression(&volume.base)
            )
        })
        .collect::<Vec<_>>()
        .join(",");
    let bones = joint
        .bones
        .iter()
        .map(joint_expression)
        .collect::<Vec<_>>()
        .join(",");
    format!(
        "crate::skeleton::Joint::from_parts({}, vec![{collision_volumes}], vec![{bones}], {}, {:?}.to_owned(), {})",
        base_expression(&joint.base),
        vector_expression(&joint.pivot),
        joint.aliases,
        joint.connected
    )
}

fn render_skeleton(skeleton: &SkeletonRecord) -> String {
    format!(
        "#![allow(clippy::float_cmp, clippy::too_many_lines, clippy::unreadable_literal)]\n\n\
         pub(crate) fn default_skeleton() -> crate::skeleton::LindenSkeleton {{\n\
             crate::skeleton::LindenSkeleton::from_parts({}, {}, false, {:?}.to_owned(), {:?}.to_owned())\n\
         }}\n\n\
         #[cfg(test)] mod tests {{ use super::*; #[test] fn generated_default_shape() {{ let skeleton = default_skeleton(); assert_eq!(skeleton.version(), 2.0); assert_eq!(skeleton.num_bones(), \"133\"); assert_eq!(skeleton.num_collision_volumes(), \"26\"); assert_eq!(skeleton.bone().base.name(), \"mPelvis\"); }} }}\n",
        joint_expression(&skeleton.root),
        rust_f32(skeleton.version),
        skeleton.num_bones,
        skeleton.num_collision_volumes
    )
}

fn render_attentions(default: &[AttentionSetRecord], updated: &[AttentionSetRecord]) -> String {
    fn sets_expression(sets: &[AttentionSetRecord]) -> String {
        sets.iter()
            .map(|set| {
                let entries = set
                    .entries
                    .iter()
                    .map(|entry| {
                        format!(
                            "AttentionData {{ priority: {}, timeout: {} }}",
                            rust_f32(entry.priority),
                            rust_f32(entry.timeout)
                        )
                    })
                    .collect::<Vec<_>>()
                    .join(",");
                format!(
                    "AttentionSet {{ name: {:?}.to_owned(), entries: vec![{entries}] }}",
                    set.name
                )
            })
            .collect::<Vec<_>>()
            .join(",")
    }

    format!(
        "#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::should_implement_trait)]\n\n\
         use std::sync::OnceLock;\n\n\
         #[derive(Clone, Debug, Default, PartialEq)] pub struct AttentionData {{ priority: f32, timeout: f32 }}\n\
         impl AttentionData {{ pub fn priority(&self) -> f32 {{ self.priority }} pub fn set_priority(&mut self, value: f32) {{ self.priority = value; }} pub fn timeout(&self) -> f32 {{ self.timeout }} pub fn set_timeout(&mut self, value: f32) {{ self.timeout = value; }} }}\n\n\
         #[derive(Clone, Debug, Default, PartialEq)] pub struct AttentionSet {{ name: String, entries: Vec<AttentionData> }}\n\
         impl AttentionSet {{ pub fn get(&self, type_: crate::LookAtType) -> Result<AttentionData, crate::Error> {{ self.entries.get(type_ as usize).cloned().ok_or(crate::Error::IndexOutOfRange) }} pub fn entries(&self) -> Vec<AttentionData> {{ self.entries.clone() }} pub fn set_entries(&mut self, value: Vec<AttentionData>) {{ self.entries = value; }} pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} }}\n\n\
         fn default_sets() -> &'static [AttentionSet] {{ static SETS: OnceLock<Vec<AttentionSet>> = OnceLock::new(); SETS.get_or_init(|| vec![{}]).as_slice() }}\n\
         fn updated_sets() -> &'static [AttentionSet] {{ static SETS: OnceLock<Vec<AttentionSet>> = OnceLock::new(); SETS.get_or_init(|| vec![{}]).as_slice() }}\n\n\
         pub struct LindenAttentions; impl LindenAttentions {{ pub fn default() -> Vec<AttentionSet> {{ default_sets().to_vec() }} pub fn updated() -> Vec<AttentionSet> {{ updated_sets().to_vec() }} pub fn default_masculine() -> AttentionSet {{ default_sets()[0].clone() }} pub fn default_feminine() -> AttentionSet {{ default_sets()[1].clone() }} pub fn updated_masculine() -> AttentionSet {{ updated_sets()[0].clone() }} pub fn updated_feminine() -> AttentionSet {{ updated_sets()[1].clone() }} }}\n\n\
         #[cfg(test)] mod tests {{ use super::*; #[test] fn generated_attention_shape() {{ assert_eq!(default_sets().len(), 2); assert_eq!(updated_sets().len(), 2); assert!(default_sets().iter().chain(updated_sets()).all(|set| set.entries.len() == 11)); assert_eq!(default_sets()[0].name, \"Masculine\"); assert_eq!(default_sets()[1].name, \"Feminine\"); }} }}\n",
        sets_expression(default),
        sets_expression(updated)
    )
}

fn render_genepool(archetypes: &[ArchetypeRecord]) -> String {
    let values = archetypes
        .iter()
        .map(|archetype| {
            let params = archetype
                .params
                .iter()
                .map(|param| {
                    format!(
                        "ArchetypeParam {{ id: {}, name: {:?}.to_owned(), value: {} }}",
                        param.id,
                        param.name,
                        rust_f32(param.value)
                    )
                })
                .collect::<Vec<_>>()
                .join(",");
            format!(
                "GenepoolArchetype {{ name: {:?}.to_owned(), params: vec![{params}] }}",
                archetype.name
            )
        })
        .collect::<Vec<_>>()
        .join(",");
    format!(
        "#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::needless_pass_by_value, clippy::too_many_lines)]\n\n\
         use std::sync::OnceLock;\n\n\
         #[derive(Clone, Debug, Default, PartialEq)] pub struct ArchetypeParam {{ id: i32, name: String, value: f32 }}\n\
         impl ArchetypeParam {{ pub fn id(&self) -> i32 {{ self.id }} pub fn set_id(&mut self, value: i32) {{ self.id = value; }} pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} pub fn value(&self) -> f32 {{ self.value }} pub fn set_value(&mut self, value: f32) {{ self.value = value; }} }}\n\n\
         #[derive(Clone, Debug, Default, PartialEq)] pub struct GenepoolArchetype {{ name: String, params: Vec<ArchetypeParam> }}\n\
         impl GenepoolArchetype {{ pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} pub fn params(&self) -> Vec<ArchetypeParam> {{ self.params.clone() }} pub fn set_params(&mut self, value: Vec<ArchetypeParam>) {{ self.params = value; }} }}\n\n\
         fn generated_archetypes() -> &'static [GenepoolArchetype] {{ static ARCHETYPES: OnceLock<Vec<GenepoolArchetype>> = OnceLock::new(); ARCHETYPES.get_or_init(|| vec![{values}]).as_slice() }}\n\n\
         pub struct Genepool; impl Genepool {{ pub fn archetypes() -> Vec<GenepoolArchetype> {{ generated_archetypes().to_vec() }} pub fn find_index(name: String) -> Result<i32, crate::Error> {{ let Some(index) = generated_archetypes().iter().position(|archetype| archetype.name == name) else {{ return Ok(-1); }}; i32::try_from(index).map_err(|_| crate::Error::IndexOutOfRange) }} pub fn get(index: i32) -> Result<GenepoolArchetype, crate::Error> {{ usize::try_from(index).ok().and_then(|index| generated_archetypes().get(index)).cloned().ok_or(crate::Error::IndexOutOfRange) }} }}\n\n\
         #[cfg(test)] mod tests {{ use super::*; #[test] fn generated_genepool_shape() {{ let archetypes = generated_archetypes(); assert_eq!(archetypes.len(), 24); assert_eq!(archetypes.iter().map(|archetype| archetype.params.len()).sum::<usize>(), 3360); assert_eq!(archetypes[0].name, \"M W Skinny\"); }} }}\n"
    )
}

fn load_input<'a>(inventory: &'a super::Inventory, id: &str) -> Result<&'a InputSpec, String> {
    inventory
        .inputs
        .iter()
        .find(|input| input.id == id)
        .ok_or_else(|| format!("source inventory has no {id} input"))
}

fn load_text(root: &Path, input: &InputSpec) -> Result<String, String> {
    let path = root.join(&input.vendored_path);
    let bytes = fs::read(&path).map_err(|error| format!("{}: {error}", path.display()))?;
    normalize_text(&input.vendored_path, &bytes).map_err(|error| error.to_string())
}

fn format_body(body: &str, name: &str) -> Result<String, String> {
    let syntax = syn::parse_file(body)
        .map_err(|error| format!("generated {name} Rust is invalid: {error}"))?;
    Ok(prettyplease::unparse(&syntax))
}

pub(super) fn skeleton_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
    let inventory = super::load_inventory(root)?;
    verify_inputs(root, &inventory)?;
    let input = load_input(&inventory, "avatar_skeleton")?;
    let skeleton = parse_skeleton(&input.vendored_path, &load_text(root, input)?)?;
    let body = format_body(&render_skeleton(&skeleton), "skeleton")?;
    Ok(generated_rust("skeleton", &[input], &body))
}

pub(super) fn attention_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
    let inventory = super::load_inventory(root)?;
    verify_inputs(root, &inventory)?;
    let default_input = load_input(&inventory, "attentions")?;
    let updated_input = load_input(&inventory, "attentions_updated")?;
    let default = parse_attentions(
        &default_input.vendored_path,
        &load_text(root, default_input)?,
    )?;
    let updated = parse_attentions(
        &updated_input.vendored_path,
        &load_text(root, updated_input)?,
    )?;
    let body = format_body(&render_attentions(&default, &updated), "attention")?;
    Ok(generated_rust(
        "attentions",
        &[default_input, updated_input],
        &body,
    ))
}

pub(super) fn genepool_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
    let inventory = super::load_inventory(root)?;
    verify_inputs(root, &inventory)?;
    let input = load_input(&inventory, "genepool")?;
    let archetypes = parse_genepool(&input.vendored_path, &load_text(root, input)?)?;
    let body = format_body(&render_genepool(&archetypes), "genepool")?;
    Ok(generated_rust("genepool", &[input], &body))
}

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

    #[test]
    fn skeleton_schema_rejects_bad_counts_vectors_and_duplicate_names() {
        let bad_count = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"/></linden_skeleton>";
        assert!(
            parse_skeleton("bad.xml", bad_count)
                .unwrap_err()
                .contains("error[SK006]")
        );
        let bad_vector = "<linden_skeleton version=\"1\" num_bones=\"1\" num_collision_volumes=\"0\"><bone name=\"root\" pos=\"1 2\"/></linden_skeleton>";
        assert!(
            parse_skeleton("bad.xml", bad_vector)
                .unwrap_err()
                .contains("error[SK011]")
        );
        let duplicate = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"><bone name=\"root\"/></bone></linden_skeleton>";
        assert!(
            parse_skeleton("bad.xml", duplicate)
                .unwrap_err()
                .contains("error[SK016]")
        );
    }

    #[test]
    fn attention_schema_rejects_unknown_and_incomplete_sets() {
        let unknown = "<linden_attentions version=\"1\"><gender name=\"Masculine\"><param attention=\"unknown\" priority=\"1\" timeout=\"1\"/></gender><gender name=\"Feminine\"/></linden_attentions>";
        assert!(
            parse_attentions("bad.xml", unknown)
                .unwrap_err()
                .contains("error[AT009]")
        );
        let incomplete = "<linden_attentions version=\"1\"><gender name=\"Masculine\"/><gender name=\"Feminine\"/></linden_attentions>";
        assert!(
            parse_attentions("bad.xml", incomplete)
                .unwrap_err()
                .contains("error[AT013]")
        );
    }

    #[test]
    fn genepool_schema_rejects_bad_values_and_duplicate_ids() {
        let bad_value = "<linden_genepool version=\"1\"><archetype name=\"A\"><param id=\"1\" name=\"P\" value=\"NaN\"/></archetype></linden_genepool>";
        assert!(
            parse_genepool("bad.xml", bad_value)
                .unwrap_err()
                .contains("error[GP010]")
        );
        let duplicate = "<linden_genepool version=\"1\"><archetype name=\"A\"><param id=\"1\" name=\"P\" value=\"0\"/><param id=\"1\" name=\"Q\" value=\"1\"/></archetype></linden_genepool>";
        assert!(
            parse_genepool("bad.xml", duplicate)
                .unwrap_err()
                .contains("error[GP008]")
        );
    }

    #[test]
    fn pinned_avatar_catalog_semantics_have_stable_digests() {
        let root = super::super::workspace_root();
        let inventory = super::super::load_inventory(&root).expect("pinned inventory");
        let skeleton_input = load_input(&inventory, "avatar_skeleton").expect("skeleton input");
        let skeleton = parse_skeleton(
            &skeleton_input.vendored_path,
            &load_text(&root, skeleton_input).expect("skeleton text"),
        )
        .expect("skeleton");
        let attentions_input = load_input(&inventory, "attentions").expect("attentions input");
        let updated_input =
            load_input(&inventory, "attentions_updated").expect("updated attentions input");
        let default = parse_attentions(
            &attentions_input.vendored_path,
            &load_text(&root, attentions_input).expect("attentions text"),
        )
        .expect("attentions");
        let updated = parse_attentions(
            &updated_input.vendored_path,
            &load_text(&root, updated_input).expect("updated attentions text"),
        )
        .expect("updated attentions");
        let genepool_input = load_input(&inventory, "genepool").expect("genepool input");
        let genepool = parse_genepool(
            &genepool_input.vendored_path,
            &load_text(&root, genepool_input).expect("genepool text"),
        )
        .expect("genepool");

        assert_eq!(
            super::super::sha256(format!("{skeleton:#?}").as_bytes()),
            "912dc35e06ac8f2eed48819f340ea18ab6173437cc251cda52081bc8445db4d6"
        );
        assert_eq!(
            super::super::sha256(format!("{default:#?}\n{updated:#?}").as_bytes()),
            "a0343e5d473ce2bfa29985e5d98afb066bf282b8786ba099aa76457b0b3d8af8"
        );
        assert_eq!(
            super::super::sha256(format!("{genepool:#?}").as_bytes()),
            "5e33fecab1a9501113096ee5ec38f52eadb7deb3de05a7c50c42e00c15c24a5b"
        );
    }
}
