//! Schema-validated generators for the pinned avatar and foliage XML catalogs.

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

#[derive(Clone, Debug, PartialEq)]
struct AlphaDefinition {
    domain: f32,
    tga_file: String,
    skip_if_zero: bool,
    multiply_blend: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ColorOperation {
    Add,
    Blend,
}

#[derive(Clone, Debug, PartialEq)]
struct ColorDefinition {
    operation: ColorOperation,
    colors: Vec<[u8; 4]>,
}

#[derive(Clone, Debug, PartialEq)]
struct DrivenDefinition {
    param_id: i32,
    min1: f32,
    max1: f32,
    max2: f32,
    min2: f32,
    has_range: bool,
}

#[derive(Clone, Debug, PartialEq)]
struct SkeletalDefinition {
    bone_name: String,
    scale: [f32; 3],
    position: [f32; 3],
    has_position: bool,
}

#[derive(Clone, Debug, PartialEq)]
struct VolumeMorphDefinition {
    bone_name: String,
    scale: [f32; 3],
    has_scale: bool,
    position: [f32; 3],
    has_position: bool,
}

#[derive(Clone, Debug, PartialEq)]
struct VisualDefinition {
    param_id: i32,
    name: String,
    group: i32,
    wearable: Option<String>,
    label: String,
    label_min: String,
    label_max: String,
    default_value: f32,
    min_value: f32,
    max_value: f32,
    is_bump_attribute: bool,
    alpha: Option<AlphaDefinition>,
    color: Option<ColorDefinition>,
    driven: Vec<DrivenDefinition>,
    skeletal: Vec<SkeletalDefinition>,
    volume_morphs: Vec<VolumeMorphDefinition>,
}

#[derive(Clone, Debug, PartialEq)]
struct VisualCatalog {
    params: BTreeMap<i32, VisualDefinition>,
    transmitted_ids: Vec<i32>,
}

#[derive(Clone, Debug, PartialEq)]
struct TreeDefinition {
    name: String,
    species_id: u8,
    texture_id: String,
    droop: f32,
    twist: f32,
    branches: f32,
    depth: i32,
    scale_step: f32,
    trunk_depth: f32,
    branch_length: f32,
    trunk_length: f32,
    leaf_scale: f32,
    billboard_scale: f32,
    billboard_ratio: f32,
    trunk_aspect: f32,
    branch_aspect: f32,
    leaf_rotate: f32,
    noise_mag: f32,
    noise_scale: f32,
    taper: f32,
    repeat_z: i32,
}

#[derive(Clone, Debug, PartialEq)]
struct GrassDefinition {
    name: String,
    species_id: u8,
    texture_id: String,
    blade_size_x: f32,
    blade_size_y: f32,
}

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).map(str::to_owned).ok_or_else(|| {
        location(
            path,
            node,
            code,
            &format!("<{}> requires attribute {name:?}", node.tag_name().name()),
        )
    })
}

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_u8(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result<u8, String> {
    let value = required_attribute(path, node, name, code)?;
    value.parse::<u8>().map_err(|_| {
        location(
            path,
            node,
            code,
            &format!("attribute {name:?} must be an unsigned byte, found {value:?}"),
        )
    })
}

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 optional_f32(
    path: &str,
    node: Node<'_, '_>,
    name: &str,
    default: f32,
    code: &str,
) -> Result<f32, String> {
    if node.attribute(name).is_some() {
        parse_f32(path, node, name, code)
    } else {
        Ok(default)
    }
}

fn true_attribute(node: Node<'_, '_>, name: &str) -> bool {
    node.attribute(name)
        .is_some_and(|value| value.eq_ignore_ascii_case("true"))
}

fn parse_vector(
    path: &str,
    node: Node<'_, '_>,
    name: &str,
    code: &str,
) -> Result<[f32; 3], String> {
    let value = required_attribute(path, node, name, code)?;
    let components = 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, found {value:?}"),
            )
        })?;
    if components.len() != 3 || components.iter().any(|component| !component.is_finite()) {
        return Err(location(
            path,
            node,
            code,
            &format!("attribute {name:?} must contain three finite floats, found {value:?}"),
        ));
    }
    Ok([components[0], components[1], components[2]])
}

fn parse_color(path: &str, node: Node<'_, '_>) -> Result<[u8; 4], String> {
    let value = required_attribute(path, node, "color", "VP021")?;
    let components = value
        .split(|character: char| character == ',' || character.is_ascii_whitespace())
        .filter(|component| !component.is_empty())
        .map(str::parse::<u8>)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|_| {
            location(
                path,
                node,
                "VP021",
                &format!("color must contain four bytes, found {value:?}"),
            )
        })?;
    if components.len() != 4 {
        return Err(location(
            path,
            node,
            "VP021",
            &format!("color must contain four bytes, found {value:?}"),
        ));
    }
    Ok([components[0], components[1], components[2], components[3]])
}

#[allow(clippy::too_many_lines)] // The schema mirrors all golden visual-param child variants.
fn parse_visual_catalog(path: &str, text: &str) -> Result<VisualCatalog, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("linden_avatar") {
        return Err(location(
            path,
            root,
            "VP001",
            "root element must be <linden_avatar>",
        ));
    }

    let mut params = BTreeMap::new();
    let mut transmitted_ids = Vec::new();
    for node in root
        .descendants()
        .filter(|node| node.is_element() && node.has_tag_name("param"))
    {
        let Some(group_text) = node.attribute("group") else {
            continue;
        };
        if node.attribute("shared") == Some("1") {
            continue;
        }
        let param_id = parse_i32(path, node, "id", "VP010")?;
        let name = required_attribute(path, node, "name", "VP011")?;
        let group = group_text.parse::<i32>().map_err(|_| {
            location(
                path,
                node,
                "VP012",
                &format!(
                    "attribute \"group\" must be a signed 32-bit integer, found {group_text:?}"
                ),
            )
        })?;
        if params.contains_key(&param_id) {
            return Err(location(
                path,
                node,
                "VP013",
                &format!("duplicate visual parameter ID {param_id}"),
            ));
        }
        let min_value = parse_f32(path, node, "value_min", "VP014")?;
        let max_value = parse_f32(path, node, "value_max", "VP015")?;
        if min_value > max_value {
            return Err(location(
                path,
                node,
                "VP016",
                &format!("visual parameter {param_id} has value_min greater than value_max"),
            ));
        }
        let default_value = optional_f32(path, node, "value_default", 0.0, "VP017")?;

        let parent_layer = node.parent().filter(|parent| parent.has_tag_name("layer"));
        let is_bump_attribute =
            parent_layer.and_then(|parent| parent.attribute("render_pass")) == Some("bump");
        let skip_color = parent_layer.is_some_and(|parent| {
            parent.children().any(|child| {
                child.has_tag_name("texture")
                    && child
                        .attribute("local_texture_alpha_only")
                        .is_some_and(|value| value.eq_ignore_ascii_case("true"))
            })
        });

        let mut alpha = None;
        let mut color = None;
        let mut driven = Vec::new();
        let mut skeletal = Vec::new();
        let mut volume_morphs = Vec::new();
        for child in node.children().filter(Node::is_element) {
            if child.has_tag_name("param_alpha") {
                if alpha.is_some() {
                    return Err(location(path, child, "VP018", "duplicate <param_alpha>"));
                }
                alpha = Some(AlphaDefinition {
                    domain: optional_f32(path, child, "domain", 0.0, "VP019")?,
                    tga_file: child.attribute("tga_file").unwrap_or_default().to_owned(),
                    skip_if_zero: true_attribute(child, "skip_if_zero"),
                    multiply_blend: true_attribute(child, "multiply_blend"),
                });
            } else if child.has_tag_name("param_color") {
                if color.is_some() {
                    return Err(location(path, child, "VP020", "duplicate <param_color>"));
                }
                let operation = match child.attribute("operation") {
                    None => ColorOperation::Add,
                    Some("blend" | "multiply") => ColorOperation::Blend,
                    Some(value) => {
                        return Err(location(
                            path,
                            child,
                            "VP022",
                            &format!("unknown color operation {value:?}"),
                        ));
                    }
                };
                let colors = child
                    .children()
                    .filter(|value| value.is_element() && value.has_tag_name("value"))
                    .map(|value| parse_color(path, value))
                    .collect::<Result<Vec<_>, _>>()?;
                if colors.is_empty() {
                    return Err(location(
                        path,
                        child,
                        "VP023",
                        "<param_color> requires at least one <value color=...>",
                    ));
                }
                if !skip_color {
                    color = Some(ColorDefinition { operation, colors });
                }
            } else if child.has_tag_name("param_driver") {
                for driven_node in child
                    .children()
                    .filter(|value| value.is_element() && value.has_tag_name("driven"))
                {
                    let driven_id = parse_i32(path, driven_node, "id", "VP030")?;
                    let has_range = driven_node.attribute("min1").is_some();
                    let item = if has_range {
                        DrivenDefinition {
                            param_id: driven_id,
                            min1: parse_f32(path, driven_node, "min1", "VP031")?,
                            max1: parse_f32(path, driven_node, "max1", "VP032")?,
                            max2: parse_f32(path, driven_node, "max2", "VP033")?,
                            min2: parse_f32(path, driven_node, "min2", "VP034")?,
                            has_range: true,
                        }
                    } else {
                        for attribute in ["max1", "max2", "min2"] {
                            if driven_node.attribute(attribute).is_some() {
                                return Err(location(
                                    path,
                                    driven_node,
                                    "VP035",
                                    "driven range attributes must be supplied together",
                                ));
                            }
                        }
                        DrivenDefinition {
                            param_id: driven_id,
                            min1: 0.0,
                            max1: 0.0,
                            max2: 0.0,
                            min2: 0.0,
                            has_range: false,
                        }
                    };
                    driven.push(item);
                }
            } else if child.has_tag_name("param_skeleton") {
                for bone in child
                    .children()
                    .filter(|value| value.is_element() && value.has_tag_name("bone"))
                {
                    let position = bone
                        .attribute("offset")
                        .map(|_| parse_vector(path, bone, "offset", "VP041"))
                        .transpose()?
                        .unwrap_or([0.0; 3]);
                    skeletal.push(SkeletalDefinition {
                        bone_name: required_attribute(path, bone, "name", "VP040")?,
                        scale: parse_vector(path, bone, "scale", "VP042")?,
                        position,
                        has_position: bone.attribute("offset").is_some(),
                    });
                }
            } else if child.has_tag_name("param_morph") {
                for morph in child
                    .children()
                    .filter(|value| value.is_element() && value.has_tag_name("volume_morph"))
                {
                    let scale = morph
                        .attribute("scale")
                        .map(|_| parse_vector(path, morph, "scale", "VP051"))
                        .transpose()?
                        .unwrap_or([0.0; 3]);
                    let position = morph
                        .attribute("pos")
                        .map(|_| parse_vector(path, morph, "pos", "VP052"))
                        .transpose()?
                        .unwrap_or([0.0; 3]);
                    volume_morphs.push(VolumeMorphDefinition {
                        bone_name: required_attribute(path, morph, "name", "VP050")?,
                        scale,
                        has_scale: morph.attribute("scale").is_some(),
                        position,
                        has_position: morph.attribute("pos").is_some(),
                    });
                }
            }
        }

        if matches!(group, 0 | 3) {
            transmitted_ids.push(param_id);
        }
        params.insert(
            param_id,
            VisualDefinition {
                param_id,
                name,
                group,
                wearable: node.attribute("wearable").map(str::to_owned),
                label: node.attribute("label").unwrap_or_default().to_owned(),
                label_min: node.attribute("label_min").unwrap_or_default().to_owned(),
                label_max: node.attribute("label_max").unwrap_or_default().to_owned(),
                default_value,
                min_value,
                max_value,
                is_bump_attribute,
                alpha,
                color,
                driven,
                skeletal,
                volume_morphs,
            },
        );
    }
    transmitted_ids.sort_unstable();
    if params.is_empty() || transmitted_ids.is_empty() {
        return Err(location(
            path,
            root,
            "VP060",
            "visual parameter catalog must not be empty",
        ));
    }
    for definition in params.values() {
        for driven in &definition.driven {
            if !params.contains_key(&driven.param_id) {
                return Err(location(
                    path,
                    root,
                    "VP061",
                    &format!(
                        "visual parameter {} drives missing parameter {}",
                        definition.param_id, driven.param_id
                    ),
                ));
            }
        }
    }
    Ok(VisualCatalog {
        params,
        transmitted_ids,
    })
}

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 parse_trees(path: &str, text: &str) -> Result<Vec<TreeDefinition>, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("tree_defs") {
        return Err(location(
            path,
            root,
            "TR001",
            "root element must be <tree_defs>",
        ));
    }
    let mut definitions = Vec::new();
    let mut ids = BTreeSet::new();
    for node in root.children().filter(Node::is_element) {
        if !node.has_tag_name("tree") {
            return Err(location(
                path,
                node,
                "TR002",
                "<tree_defs> may contain only <tree> elements",
            ));
        }
        let species_id = parse_u8(path, node, "species_id", "TR003")?;
        if !ids.insert(species_id) {
            return Err(location(
                path,
                node,
                "TR004",
                &format!("duplicate tree species ID {species_id}"),
            ));
        }
        let texture_id = required_attribute(path, node, "texture_id", "TR005")?;
        if !valid_uuid(&texture_id) {
            return Err(location(
                path,
                node,
                "TR006",
                &format!("invalid tree texture UUID {texture_id:?}"),
            ));
        }
        definitions.push(TreeDefinition {
            name: required_attribute(path, node, "name", "TR007")?,
            species_id,
            texture_id,
            droop: parse_f32(path, node, "droop", "TR010")?,
            twist: parse_f32(path, node, "twist", "TR011")?,
            branches: parse_f32(path, node, "branches", "TR012")?,
            depth: parse_i32(path, node, "depth", "TR013")?,
            scale_step: parse_f32(path, node, "scale_step", "TR014")?,
            trunk_depth: parse_f32(path, node, "trunk_depth", "TR015")?,
            branch_length: parse_f32(path, node, "branch_length", "TR016")?,
            trunk_length: parse_f32(path, node, "trunk_length", "TR017")?,
            leaf_scale: parse_f32(path, node, "leaf_scale", "TR018")?,
            billboard_scale: parse_f32(path, node, "billboard_scale", "TR019")?,
            billboard_ratio: parse_f32(path, node, "billboard_ratio", "TR020")?,
            trunk_aspect: parse_f32(path, node, "trunk_aspect", "TR021")?,
            branch_aspect: parse_f32(path, node, "branch_aspect", "TR022")?,
            leaf_rotate: parse_f32(path, node, "leaf_rotate", "TR023")?,
            noise_mag: parse_f32(path, node, "noise_mag", "TR024")?,
            noise_scale: parse_f32(path, node, "noise_scale", "TR025")?,
            taper: parse_f32(path, node, "taper", "TR026")?,
            repeat_z: parse_i32(path, node, "repeat_z", "TR027")?,
        });
    }
    validate_contiguous(
        path,
        root,
        "tree",
        definitions.iter().map(|item| item.species_id),
    )?;
    Ok(definitions)
}

fn parse_grass(path: &str, text: &str) -> Result<Vec<GrassDefinition>, String> {
    let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
    let root = document.root_element();
    if !root.has_tag_name("grass_defs") {
        return Err(location(
            path,
            root,
            "GR001",
            "root element must be <grass_defs>",
        ));
    }
    let mut definitions = Vec::new();
    let mut ids = BTreeSet::new();
    for node in root.children().filter(Node::is_element) {
        if !node.has_tag_name("grass") {
            return Err(location(
                path,
                node,
                "GR002",
                "<grass_defs> may contain only <grass> elements",
            ));
        }
        let species_id = parse_u8(path, node, "species_id", "GR003")?;
        if !ids.insert(species_id) {
            return Err(location(
                path,
                node,
                "GR004",
                &format!("duplicate grass species ID {species_id}"),
            ));
        }
        let texture_id = required_attribute(path, node, "texture_id", "GR005")?;
        if !valid_uuid(&texture_id) {
            return Err(location(
                path,
                node,
                "GR006",
                &format!("invalid grass texture UUID {texture_id:?}"),
            ));
        }
        definitions.push(GrassDefinition {
            name: required_attribute(path, node, "name", "GR007")?,
            species_id,
            texture_id,
            blade_size_x: parse_f32(path, node, "blade_size_x", "GR010")?,
            blade_size_y: parse_f32(path, node, "blade_size_y", "GR011")?,
        });
    }
    validate_contiguous(
        path,
        root,
        "grass",
        definitions.iter().map(|item| item.species_id),
    )?;
    Ok(definitions)
}

fn validate_contiguous(
    path: &str,
    root: Node<'_, '_>,
    name: &str,
    ids: impl Iterator<Item = u8>,
) -> Result<(), String> {
    let ids = ids.collect::<Vec<_>>();
    if ids.is_empty() {
        return Err(location(
            path,
            root,
            "FG020",
            &format!("{name} catalog must not be empty"),
        ));
    }
    if ids
        .iter()
        .copied()
        .enumerate()
        .any(|(index, id)| usize::from(id) != index)
    {
        return Err(location(
            path,
            root,
            "FG021",
            &format!("{name} species IDs must be contiguous and in source order from zero"),
        ));
    }
    Ok(())
}

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

fn vector_expression(value: [f32; 3]) -> String {
    format!(
        "libremetaverse_types::Vector3 {{ x: {}, y: {}, z: {} }}",
        rust_f32(value[0]),
        rust_f32(value[1]),
        rust_f32(value[2])
    )
}

fn option_string(value: Option<&str>) -> String {
    value.map_or_else(
        || "None".to_owned(),
        |value| format!("Some({value:?}.to_owned())"),
    )
}

#[allow(clippy::too_many_lines)] // One renderer keeps the generated public type/data contract atomic.
fn render_visual_catalog(catalog: &VisualCatalog) -> String {
    let mut body = String::new();
    body.push_str(
        "#![allow(clippy::float_cmp, clippy::manual_string_new, clippy::missing_errors_doc, clippy::must_use_candidate, clippy::needless_pass_by_value, clippy::too_many_lines, clippy::unnecessary_wraps)]\n\n\
         use std::collections::{BTreeMap, HashMap};\n\
         use std::sync::OnceLock;\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct VisualAlphaParam { pub domain: f32, pub multiply_blend: bool, pub skip_if_zero: bool, pub tga_file: String }\n\
         impl VisualAlphaParam { pub fn new(domain: f32, tga_file: String, skip_if_zero: bool, multiply_blend: bool) -> Result<Self, crate::Error> { Ok(Self { domain, multiply_blend, skip_if_zero, tga_file }) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct VisualColorParam { pub colors: Vec<libremetaverse_types::Color4>, pub operation: crate::VisualColorOperation }\n\
         impl VisualColorParam { pub fn new(operation: crate::VisualColorOperation, colors: Vec<libremetaverse_types::Color4>) -> Result<Self, crate::Error> { Ok(Self { colors, operation }) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct DrivenParamInfo { pub has_range: bool, pub max1: f32, pub max2: f32, pub min1: f32, pub min2: f32, pub param_id: i32 }\n\
         impl DrivenParamInfo { pub fn new(param_id: i32, min1: f32, max1: f32, max2: f32, min2: f32, has_range: bool) -> Result<Self, crate::Error> { Ok(Self { has_range, max1, max2, min1, min2, param_id }) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct SkeletalBoneInfo { pub bone_name: String, pub has_position_deformation: bool, pub position_deformation: libremetaverse_types::Vector3, pub scale_deformation: libremetaverse_types::Vector3 }\n\
         impl SkeletalBoneInfo { pub fn new(bone_name: String, scale_deformation: libremetaverse_types::Vector3, position_deformation: libremetaverse_types::Vector3, has_position_deformation: bool) -> Result<Self, crate::Error> { Ok(Self { bone_name, has_position_deformation, position_deformation, scale_deformation }) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct VolumeMorphInfo { pub bone_name: String, pub has_position: bool, pub has_scale: bool, pub position_delta: libremetaverse_types::Vector3, pub scale_delta: libremetaverse_types::Vector3 }\n\
         impl VolumeMorphInfo { pub fn new(bone_name: String, scale_delta: libremetaverse_types::Vector3, has_scale: bool, position_delta: libremetaverse_types::Vector3, has_position: bool) -> Result<Self, crate::Error> { Ok(Self { bone_name, has_position, has_scale, position_delta, scale_delta }) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct VisualParam {\n\
             pub alpha_params: Option<Option<VisualAlphaParam>>, pub color_params: Option<Option<VisualColorParam>>,\n\
             pub default_value: f32, pub driven_params: Option<Vec<DrivenParamInfo>>, pub drivers: Option<Vec<i32>>,\n\
             pub group: i32, pub is_bump_attribute: bool, pub label: String, pub label_max: String, pub label_min: String,\n\
             pub max_value: f32, pub min_value: f32, pub name: String, pub param_id: i32,\n\
             pub skeletal_distortions: Option<Vec<SkeletalBoneInfo>>, pub volume_morphs: Option<Vec<VolumeMorphInfo>>, pub wearable: Option<String>,\n\
         }\n\
         impl Default for VisualParam { fn default() -> Self { Self { alpha_params: None, color_params: None, default_value: 0.0, driven_params: None, drivers: None, group: 0, is_bump_attribute: false, label: String::new(), label_max: String::new(), label_min: String::new(), max_value: 0.0, min_value: 0.0, name: String::new(), param_id: 0, skeletal_distortions: None, volume_morphs: None, wearable: None } } }\n\
         impl VisualParam {\n\
             #[allow(clippy::too_many_arguments)]\n\
             pub fn new(param_id: i32, name: String, group: i32, wearable: Option<String>, label: String, label_min: String, label_max: String, def_: f32, min: f32, max: f32, is_bump_attribute: bool, drivers: Option<Vec<i32>>, alpha: Option<Option<VisualAlphaParam>>, color_params: Option<Option<VisualColorParam>>, driven_params: Option<Vec<DrivenParamInfo>>, skeletal_distortions: Option<Vec<SkeletalBoneInfo>>, volume_morphs: Option<Vec<VolumeMorphInfo>>) -> Result<Self, crate::Error> { Ok(Self { alpha_params: alpha, color_params, default_value: def_, driven_params, drivers, group, is_bump_attribute, label, label_max, label_min, max_value: max, min_value: min, name, param_id, skeletal_distortions, volume_morphs, wearable }) }\n\
         }\n\n",
    );
    body.push_str("const GROUP0_PARAM_IDS: &[i32] = &[");
    for id in &catalog.transmitted_ids {
        let _ = write!(body, "{id},");
    }
    body.push_str("];\n\nfn generated_params() -> &'static BTreeMap<i32, VisualParam> {\n    static PARAMS: OnceLock<BTreeMap<i32, VisualParam>> = OnceLock::new();\n    PARAMS.get_or_init(|| {\n    let mut params = BTreeMap::new();\n");
    for definition in catalog.params.values() {
        let alpha = definition.alpha.as_ref().map_or_else(
            || "None".to_owned(),
            |alpha| {
                format!(
                    "Some(Some(VisualAlphaParam {{ domain: {}, multiply_blend: {}, skip_if_zero: {}, tga_file: {:?}.to_owned() }}))",
                    rust_f32(alpha.domain), alpha.multiply_blend, alpha.skip_if_zero, alpha.tga_file
                )
            },
        );
        let color = definition.color.as_ref().map_or_else(
            || "None".to_owned(),
            |color| {
                let operation = match color.operation {
                    ColorOperation::Add => "crate::VisualColorOperation::Add",
                    ColorOperation::Blend => "crate::VisualColorOperation::Blend",
                };
                let values = color
                    .colors
                    .iter()
                    .map(|value| {
                        format!(
                            "libremetaverse_types::Color4 {{ r: f32::from({}_u8) / 255.0, g: f32::from({}_u8) / 255.0, b: f32::from({}_u8) / 255.0, a: f32::from({}_u8) / 255.0 }}",
                            value[0], value[1], value[2], value[3]
                        )
                    })
                    .collect::<Vec<_>>()
                    .join(",");
                format!("Some(Some(VisualColorParam {{ colors: vec![{values}], operation: {operation} }}))")
            },
        );
        let drivers = if definition.driven.is_empty() {
            "None".to_owned()
        } else {
            format!(
                "Some(vec![{}])",
                definition
                    .driven
                    .iter()
                    .map(|item| item.param_id.to_string())
                    .collect::<Vec<_>>()
                    .join(",")
            )
        };
        let driven = if definition.driven.is_empty() {
            "None".to_owned()
        } else {
            format!(
                "Some(vec![{}])",
                definition
                    .driven
                    .iter()
                    .map(|item| format!(
                        "DrivenParamInfo {{ has_range: {}, max1: {}, max2: {}, min1: {}, min2: {}, param_id: {} }}",
                        item.has_range,
                        rust_f32(item.max1), rust_f32(item.max2), rust_f32(item.min1), rust_f32(item.min2), item.param_id
                    ))
                    .collect::<Vec<_>>()
                    .join(",")
            )
        };
        let skeletal = if definition.skeletal.is_empty() {
            "None".to_owned()
        } else {
            format!(
                "Some(vec![{}])",
                definition
                    .skeletal
                    .iter()
                    .map(|item| format!(
                        "SkeletalBoneInfo {{ bone_name: {:?}.to_owned(), has_position_deformation: {}, position_deformation: {}, scale_deformation: {} }}",
                        item.bone_name, item.has_position, vector_expression(item.position), vector_expression(item.scale)
                    ))
                    .collect::<Vec<_>>()
                    .join(",")
            )
        };
        let volume_morphs = if definition.volume_morphs.is_empty() {
            "None".to_owned()
        } else {
            format!(
                "Some(vec![{}])",
                definition
                    .volume_morphs
                    .iter()
                    .map(|item| format!(
                        "VolumeMorphInfo {{ bone_name: {:?}.to_owned(), has_position: {}, has_scale: {}, position_delta: {}, scale_delta: {} }}",
                        item.bone_name, item.has_position, item.has_scale, vector_expression(item.position), vector_expression(item.scale)
                    ))
                    .collect::<Vec<_>>()
                    .join(",")
            )
        };
        let _ = writeln!(
            body,
            "    params.insert({}, VisualParam {{ alpha_params: {alpha}, color_params: {color}, default_value: {}, driven_params: {driven}, drivers: {drivers}, group: {}, is_bump_attribute: {}, label: {:?}.to_owned(), label_max: {:?}.to_owned(), label_min: {:?}.to_owned(), max_value: {}, min_value: {}, name: {:?}.to_owned(), param_id: {}, skeletal_distortions: {skeletal}, volume_morphs: {volume_morphs}, wearable: {} }});",
            definition.param_id,
            rust_f32(definition.default_value),
            definition.group,
            definition.is_bump_attribute,
            definition.label,
            definition.label_max,
            definition.label_min,
            rust_f32(definition.max_value),
            rust_f32(definition.min_value),
            definition.name,
            definition.param_id,
            option_string(definition.wearable.as_deref()),
        );
    }
    body.push_str(
        "    params\n    })\n}\n\n\
         pub struct VisualParams;\n\
         impl VisualParams {\n\
             pub fn group0_param_ids() -> Vec<i32> { GROUP0_PARAM_IDS.to_vec() }\n\
             pub fn params() -> libremetaverse_types::compat::SortedList<i32, VisualParam> { libremetaverse_types::compat::SortedList(generated_params().clone()) }\n\
             pub fn find(name: String, wearable: Option<String>) -> Result<VisualParam, crate::Error> { Ok(generated_params().values().find(|param| param.name == name && param.wearable == wearable).cloned().unwrap_or_default()) }\n\
         }\n\n\
         pub(crate) fn param_by_id(id: i32) -> Option<&'static VisualParam> { generated_params().get(&id) }\n\n\
         pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap<i32, f32> {\n\
             let params = generated_params();\n\
             let mut result = HashMap::with_capacity(GROUP0_PARAM_IDS.len());\n\
             for (&id, &value) in GROUP0_PARAM_IDS.iter().zip(bytes) { if let Some(param) = params.get(&id) { let range = param.max_value - param.min_value; let mut decoded = f32::from(value) * (1.0 / 255.0) * range + param.min_value; if decoded.abs() < range * (1.0 / 255.0) { decoded = 0.0; } result.insert(id, decoded); } }\n\
             for driver in params.values() {\n\
                 let Some(driven_params) = &driver.driven_params else { continue };\n\
                 let Some(&driver_value) = result.get(&driver.param_id) else { continue };\n\
                 for driven in driven_params {\n\
                     let Some(target) = params.get(&driven.param_id) else { continue };\n\
                     let normalized = if driven.has_range {\n\
                         if driver_value < driven.min1 { 0.0 } else if driver_value < driven.max1 { (driver_value - driven.min1) / (driven.max1 - driven.min1) } else if driver_value <= driven.max2 { 1.0 } else if driver_value < driven.min2 { (driven.min2 - driver_value) / (driven.min2 - driven.max2) } else { 0.0 }\n\
                     } else { let range = driver.max_value - driver.min_value; if range > 1.0e-6 { (driver_value - driver.min_value) / range } else { 0.0 } }.clamp(0.0, 1.0);\n\
                     result.insert(driven.param_id, target.min_value + normalized * (target.max_value - target.min_value));\n\
                 }\n\
             }\n\
             result\n\
         }\n",
    );
    body.push_str(
        "pub(crate) fn new_avatar() -> Result<crate::Avatar, crate::Error> {\n\
             Ok(crate::Avatar {\n\
                 base: crate::Primitive::new_with_constructor()?,\n\
                 animations: Vec::new(), appearance_flags: crate::AppearanceFlags::NONE, appearance_version: 0,\n\
                 attachments: Vec::new(), cof_version: 0, control_flags: crate::AgentManagerControlFlags(0),\n\
                 groups: Vec::new(), hover_height: libremetaverse_types::Vector3::zero(),\n\
                 profile_interests: crate::AvatarInterests { languages_text: String::new(), skills_mask: 0, skills_text: String::new(), want_to_mask: 0, want_to_text: String::new() },\n\
                 profile_properties: crate::AvatarAvatarProperties { about_text: String::new(), born_on: String::new(), charter_member: String::new(), first_life_image: libremetaverse_types::UUID::zero(), first_life_text: String::new(), flags: crate::ProfileFlags(0), partner: libremetaverse_types::UUID::zero(), profile_image: libremetaverse_types::UUID::zero(), profile_url: String::new() },\n\
                 profile_statistics: crate::AvatarStatistics { appearance_negative: 0, appearance_positive: 0, behavior_negative: 0, behavior_positive: 0, building_negative: 0, building_positive: 0, given_negative: 0, given_positive: 0 },\n\
                 visual_parameters: Vec::new(),\n\
             })\n\
         }\n\n\
         #[cfg(test)] mod tests { use super::*; #[test] fn golden_catalog_shape_and_values() { let params = generated_params(); assert_eq!(params.len(), 672); assert_eq!(GROUP0_PARAM_IDS.len(), 253); assert!(GROUP0_PARAM_IDS.windows(2).all(|ids| ids[0] < ids[1])); let height = &params[&33]; assert_eq!(height.name, \"Height\"); assert_eq!(height.group, 0); assert_eq!(height.wearable.as_deref(), Some(\"shape\")); assert_eq!(height.min_value, -2.3); assert_eq!(height.max_value, 2.0); assert!(!height.skeletal_distortions.as_ref().unwrap().is_empty()); } }\n",
    );
    body
}

fn render_foliage_catalog(trees: &[TreeDefinition], grasses: &[GrassDefinition]) -> String {
    let mut body = String::new();
    body.push_str(
        "#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::too_many_lines)]\n\n\
         use std::sync::OnceLock;\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct TreeDefinition { name: String, species_id: i32, texture_id: libremetaverse_types::UUID, droop: f32, twist: f32, branches: f32, depth: i32, scale_step: f32, trunk_depth: f32, branch_length: f32, trunk_length: f32, leaf_scale: f32, billboard_scale: f32, billboard_ratio: f32, trunk_aspect: f32, branch_aspect: f32, leaf_rotate: f32, noise_mag: f32, noise_scale: f32, taper: f32, repeat_z: i32 }\n\
         impl TreeDefinition {\n\
             pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; }\n\
             pub fn species_id(&self) -> i32 { self.species_id } pub fn set_species_id(&mut self, value: i32) { self.species_id = value; }\n\
             pub fn texture_id(&self) -> libremetaverse_types::UUID { self.texture_id } pub fn set_texture_id(&mut self, value: libremetaverse_types::UUID) { self.texture_id = value; }\n",
    );
    for name in [
        "droop",
        "twist",
        "branches",
        "scale_step",
        "trunk_depth",
        "branch_length",
        "trunk_length",
        "leaf_scale",
        "billboard_scale",
        "billboard_ratio",
        "trunk_aspect",
        "branch_aspect",
        "leaf_rotate",
        "noise_mag",
        "noise_scale",
        "taper",
    ] {
        let _ = writeln!(
            body,
            "    pub fn {name}(&self) -> f32 {{ self.{name} }} pub fn set_{name}(&mut self, value: f32) {{ self.{name} = value; }}"
        );
    }
    body.push_str("    pub fn depth(&self) -> i32 { self.depth } pub fn set_depth(&mut self, value: i32) { self.depth = value; }\n    pub fn repeat_z(&self) -> i32 { self.repeat_z } pub fn set_repeat_z(&mut self, value: i32) { self.repeat_z = value; }\n}\n\n");
    body.push_str("fn generated_trees() -> &'static [TreeDefinition] { static TREES: OnceLock<Vec<TreeDefinition>> = OnceLock::new(); TREES.get_or_init(|| vec![\n");
    for tree in trees {
        let _ = writeln!(
            body,
            "    TreeDefinition {{ name: {:?}.to_owned(), species_id: {}, texture_id: libremetaverse_types::UUID::parse({:?}.to_owned()).expect(\"validated tree UUID\"), droop: {}, twist: {}, branches: {}, depth: {}, scale_step: {}, trunk_depth: {}, branch_length: {}, trunk_length: {}, leaf_scale: {}, billboard_scale: {}, billboard_ratio: {}, trunk_aspect: {}, branch_aspect: {}, leaf_rotate: {}, noise_mag: {}, noise_scale: {}, taper: {}, repeat_z: {} }},",
            tree.name,
            tree.species_id,
            tree.texture_id,
            rust_f32(tree.droop),
            rust_f32(tree.twist),
            rust_f32(tree.branches),
            tree.depth,
            rust_f32(tree.scale_step),
            rust_f32(tree.trunk_depth),
            rust_f32(tree.branch_length),
            rust_f32(tree.trunk_length),
            rust_f32(tree.leaf_scale),
            rust_f32(tree.billboard_scale),
            rust_f32(tree.billboard_ratio),
            rust_f32(tree.trunk_aspect),
            rust_f32(tree.branch_aspect),
            rust_f32(tree.leaf_rotate),
            rust_f32(tree.noise_mag),
            rust_f32(tree.noise_scale),
            rust_f32(tree.taper),
            tree.repeat_z
        );
    }
    body.push_str(
        "]).as_slice() }\n\n\
         pub struct TreeDefinitions; impl TreeDefinitions { pub fn all() -> Vec<TreeDefinition> { generated_trees().to_vec() } pub fn get(species: crate::Tree) -> Result<TreeDefinition, crate::Error> { generated_trees().get(species as usize).cloned().ok_or(crate::Error::Argument) } }\n\n\
         #[derive(Clone, Debug, PartialEq)]\n\
         pub struct GrassDefinition { name: String, species_id: i32, texture_id: libremetaverse_types::UUID, blade_size_x: f32, blade_size_y: f32 }\n\
         impl GrassDefinition {\n\
             pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; }\n\
             pub fn species_id(&self) -> i32 { self.species_id } pub fn set_species_id(&mut self, value: i32) { self.species_id = value; }\n\
             pub fn texture_id(&self) -> libremetaverse_types::UUID { self.texture_id } pub fn set_texture_id(&mut self, value: libremetaverse_types::UUID) { self.texture_id = value; }\n\
             pub fn blade_size_x(&self) -> f32 { self.blade_size_x } pub fn set_blade_size_x(&mut self, value: f32) { self.blade_size_x = value; }\n\
             pub fn blade_size_y(&self) -> f32 { self.blade_size_y } pub fn set_blade_size_y(&mut self, value: f32) { self.blade_size_y = value; }\n\
         }\n\nfn generated_grasses() -> &'static [GrassDefinition] { static GRASSES: OnceLock<Vec<GrassDefinition>> = OnceLock::new(); GRASSES.get_or_init(|| vec![\n",
    );
    for grass in grasses {
        let _ = writeln!(
            body,
            "    GrassDefinition {{ name: {:?}.to_owned(), species_id: {}, texture_id: libremetaverse_types::UUID::parse({:?}.to_owned()).expect(\"validated grass UUID\"), blade_size_x: {}, blade_size_y: {} }},",
            grass.name,
            grass.species_id,
            grass.texture_id,
            rust_f32(grass.blade_size_x),
            rust_f32(grass.blade_size_y)
        );
    }
    body.push_str(
        "]).as_slice() }\n\n\
         pub struct GrassDefinitions; impl GrassDefinitions { pub fn all() -> Vec<GrassDefinition> { generated_grasses().to_vec() } pub fn get(species: crate::Grass) -> Result<GrassDefinition, crate::Error> { grass_by_id(species as u8) } }\n\
         pub(crate) fn grass_by_id(species: u8) -> Result<GrassDefinition, crate::Error> { generated_grasses().get(usize::from(species)).cloned().ok_or(crate::Error::Argument) }\n\n\
         #[cfg(test)] mod tests { use super::*; #[test] fn golden_foliage_shape_and_values() { let trees = TreeDefinitions::all(); assert_eq!(trees.len(), 21); assert_eq!(trees[0].name(), \"Pine 1\"); assert_eq!(trees[0].species_id(), crate::Tree::Pine1 as i32); assert_eq!(trees[20].name(), \"Kelp 2\"); assert_eq!(TreeDefinitions::get(crate::Tree::Oak).unwrap(), trees[1]); let grass = GrassDefinitions::all(); assert_eq!(grass.len(), 6); assert_eq!(grass[5].name(), \"undergrowth_1\"); assert_eq!(GrassDefinitions::get(crate::Grass::Undergrowth1).unwrap(), grass[5]); } }\n",
    );
    body
}

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 visual_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
    let inventory = super::load_inventory(root)?;
    verify_inputs(root, &inventory)?;
    let input = load_input(&inventory, "avatar_lad")?;
    let catalog = parse_visual_catalog(&input.vendored_path, &load_text(root, input)?)?;
    let body = format_body(&render_visual_catalog(&catalog), "visual parameter")?;
    Ok(generated_rust("visual-params", &[input], &body))
}

pub(super) fn foliage_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
    let inventory = super::load_inventory(root)?;
    verify_inputs(root, &inventory)?;
    let tree_input = load_input(&inventory, "trees")?;
    let grass_input = load_input(&inventory, "grass")?;
    let trees = parse_trees(&tree_input.vendored_path, &load_text(root, tree_input)?)?;
    let grasses = parse_grass(&grass_input.vendored_path, &load_text(root, grass_input)?)?;
    let body = format_body(&render_foliage_catalog(&trees, &grasses), "foliage")?;
    Ok(generated_rust("foliage", &[tree_input, grass_input], &body))
}

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

    #[test]
    fn visual_schema_rejects_malformed_values_and_missing_driver_targets() {
        let malformed = "<linden_avatar><param id=\"1\" name=\"Bad\" group=\"0\" value_min=\"x\" value_max=\"1\"/></linden_avatar>";
        assert!(
            parse_visual_catalog("bad.xml", malformed)
                .unwrap_err()
                .contains("error[VP014]")
        );
        let missing = "<linden_avatar><param id=\"1\" name=\"Driver\" group=\"0\" value_min=\"0\" value_max=\"1\"><param_driver><driven id=\"2\"/></param_driver></param></linden_avatar>";
        assert!(
            parse_visual_catalog("bad.xml", missing)
                .unwrap_err()
                .contains("error[VP061]")
        );
    }

    #[test]
    fn foliage_schema_rejects_noncontiguous_ids_and_bad_uuids() {
        let tree = "<tree_defs><tree name=\"Bad\" species_id=\"1\" texture_id=\"bad\" droop=\"0\" twist=\"0\" branches=\"0\" depth=\"0\" scale_step=\"0\" trunk_depth=\"0\" branch_length=\"0\" trunk_length=\"0\" leaf_scale=\"0\" billboard_scale=\"0\" billboard_ratio=\"0\" trunk_aspect=\"0\" branch_aspect=\"0\" leaf_rotate=\"0\" noise_mag=\"0\" noise_scale=\"0\" taper=\"0\" repeat_z=\"0\"/></tree_defs>";
        assert!(
            parse_trees("bad.xml", tree)
                .unwrap_err()
                .contains("error[TR006]")
        );
        let grass = "<grass_defs><grass name=\"Bad\" species_id=\"1\" texture_id=\"00000000-0000-0000-0000-000000000000\" blade_size_x=\"1\" blade_size_y=\"1\"/></grass_defs>";
        assert!(
            parse_grass("bad.xml", grass)
                .unwrap_err()
                .contains("error[FG021]")
        );
    }

    #[test]
    fn pinned_catalog_semantics_have_stable_digests() {
        let root = super::super::workspace_root();
        let inventory = super::super::load_inventory(&root).expect("pinned inventory");
        let visual_input = load_input(&inventory, "avatar_lad").expect("visual input");
        let visual = parse_visual_catalog(
            &visual_input.vendored_path,
            &load_text(&root, visual_input).expect("visual text"),
        )
        .expect("visual catalog");
        let tree_input = load_input(&inventory, "trees").expect("tree input");
        let trees = parse_trees(
            &tree_input.vendored_path,
            &load_text(&root, tree_input).expect("tree text"),
        )
        .expect("tree catalog");
        let grass_input = load_input(&inventory, "grass").expect("grass input");
        let grasses = parse_grass(
            &grass_input.vendored_path,
            &load_text(&root, grass_input).expect("grass text"),
        )
        .expect("grass catalog");

        assert_eq!(
            super::super::sha256(format!("{visual:#?}").as_bytes()),
            "3cdedcd412fa8f8dee4a99407968e849c55bdcdb13b883861352d042b1783a4a"
        );
        assert_eq!(
            super::super::sha256(format!("{trees:#?}\n{grasses:#?}").as_bytes()),
            "636b3a00085d68f72c8506766a7e09c37e7db41bfca1acae228ad9cdb6b5ee50"
        );
    }
}
